diff --git a/kolibri/plugins/coach/frontend/composables/__tests__/useQuizCreation.spec.js b/kolibri/plugins/coach/frontend/composables/__tests__/useQuizCreation.spec.js index a579202767c..2219ea5b292 100644 --- a/kolibri/plugins/coach/frontend/composables/__tests__/useQuizCreation.spec.js +++ b/kolibri/plugins/coach/frontend/composables/__tests__/useQuizCreation.spec.js @@ -11,8 +11,9 @@ ExamResource.saveModel = jest.fn(() => Promise.resolve({})); const VALID_EXERCISE_ID = 'af26e1b4f3b94f3e8f4f3b4f3e8f4f3a'; /** - * @param num {number} - The number of questions to create - * @param overrides {object} - Any overrides to apply to the default question + * Generates an array of mock quiz questions. + * @param {number} num - Number of questions to generate. + * @returns {Array} Array of generated quiz question objects. */ function generateQuestions(num = 0) { const qs = []; @@ -31,9 +32,10 @@ function generateQuestions(num = 0) { return qs; } -/** @param numQuestions {number} - The number of questions to create within the exercise - * @returns {Exercise} - An exercise with the given number of questions - * A helper function to mock an exercise with a given number of questions (for `resource_pool`) +/** + * Generates a mock quiz exercise with the given number of questions. + * @param {number} numQuestions - Number of questions to include in the exercise. + * @returns {object} A mock exercise object with assessmentmetadata. */ function generateExercise(numQuestions) { const assessments = generateQuestions(numQuestions); diff --git a/kolibri/plugins/coach/frontend/composables/fetchClassSyncStatus.js b/kolibri/plugins/coach/frontend/composables/fetchClassSyncStatus.js index b21df8734c5..54a037764fa 100644 --- a/kolibri/plugins/coach/frontend/composables/fetchClassSyncStatus.js +++ b/kolibri/plugins/coach/frontend/composables/fetchClassSyncStatus.js @@ -3,8 +3,8 @@ import { handleApiError } from 'kolibri/utils/appError'; /** * Fetch sync status for all members of a class. - * @param {string} classId - The classroom ID - * @returns {Promise} Array of sync status objects, or Error on failure + * @param {string} classId - The classroom ID. + * @returns {Promise} Array of sync status objects, or Error on failure. */ export function fetchClassSyncStatus(classId) { return UserSyncStatusResource.fetchCollection({ diff --git a/kolibri/plugins/coach/frontend/composables/quizCreationSpecs.js b/kolibri/plugins/coach/frontend/composables/quizCreationSpecs.js index c1e892730b8..a89138162a6 100644 --- a/kolibri/plugins/coach/frontend/composables/quizCreationSpecs.js +++ b/kolibri/plugins/coach/frontend/composables/quizCreationSpecs.js @@ -7,14 +7,14 @@ */ /** - * @typedef {Object} QuizExercise An object referencing an exercise or topic to be used - * within the `QuizSeciton.resource_pool` property. - * @property {string} id Unique ID for this exercise (aka, `exercise_id` elsewhere) - * @property {string} title The resource title - * @property {string} parent The ID of the parent contentnode - * @property {string} content_id The ID for the piece of content - * @property {bool} is_leaf Whether or not this is a leaf node (i.e. an exercise) - * @property {string} kind Exercise or Topic in our case - see: `ContentNodeKinds` + * @typedef {object} QuizExercise An object referencing an exercise or topic to be used + * within the `QuizSection.resource_pool` property. + * @property {string} id Unique ID for this exercise (aka, `exercise_id` elsewhere) + * @property {string} title The resource title + * @property {string} parent The ID of the parent contentnode + * @property {string} content_id The ID for the piece of content + * @property {boolean} is_leaf Whether or not this is a leaf node (i.e. an exercise) + * @property {string} kind Exercise or Topic in our case - see: `ContentNodeKinds` */ export const QuizExercise = { @@ -53,17 +53,18 @@ function _exercise_id_validator(value) { } /** - * @typedef {Object} QuizQuestion A particular question in a Quiz - aka an assessment item - * from an QuizExercise. - * @property {string} item A ** unique ** identifier for this question that is + * @typedef {object} QuizQuestion A particular question in a Quiz - aka an assessment item + * from a QuizExercise. + * @property {string} item A unique identifier for this question that is * a combination of : * @property {string} exercise_id The ID of the resource from which the question originates - * @property {string} question_id A *unique* identifier of this particular question within + * @property {string} question_id A unique identifier of this particular question within * the quiz -- same as the `assessment_item_id` * @property {string} title A title for the question, editable by the user * @property {number} counter_in_exercise A number assigned to separate questions which have the * same exercise title to differentiate them */ + export const QuizQuestion = { item: { type: String, @@ -103,11 +104,12 @@ export const QuizQuestion = { }; /** - * @typedef {Object} QuizSection Defines a single section of the quiz + * @typedef {object} QuizSection Defines a single section of the quiz * @property {string} section_id A unique ID for the section - this is * only used on the front-end * @property {string} section_title The title of the quiz section - * @property {string} description A text blob associated with the section + * @property {string} description A text blob associated with the + * section * @property {QuizQuestion[]} questions The list of QuizQuestion objects in the * section * @property {boolean} learners_see_fixed_order A bool flag indicating whether this @@ -143,9 +145,9 @@ function getRandomInt() { } /** - * @typedef {Object} Quiz The overall primary Quiz object + * @typedef {object} Quiz The overall primary Quiz object * @property {string} title The title of the whole quiz - * @property {QuizSection[]} question_sources A list of the QuizSection objects that make up the + * @property {QuizSection[]} question_sources The QuizSection objects that make up the quiz * @property {number} seed A random number used to seed the randomization */ export const Quiz = { diff --git a/kolibri/plugins/coach/frontend/composables/useAttendanceForm.js b/kolibri/plugins/coach/frontend/composables/useAttendanceForm.js index 843fc4556b2..94901bb72e7 100644 --- a/kolibri/plugins/coach/frontend/composables/useAttendanceForm.js +++ b/kolibri/plugins/coach/frontend/composables/useAttendanceForm.js @@ -7,16 +7,15 @@ import useCoreCoach from './useCoreCoach'; /** * Shared attendance form logic used by both AttendanceNewPage and AttendanceEditPage. - * - * @param {Object} options + * @param {object} options - Options object. * @param {import('vue').Ref|import('vue').ComputedRef} options.hasChanges * Reactive flag indicating whether the form has unsaved changes. - * @param {Function} options.markClean - * Called when the user confirms leaving without saving. + * @param {Function} options.markClean - Called when the user confirms leaving without saving. * @param {import('vue').Ref} options.submitting * Reactive flag for whether a submit/save is in progress. - * @param {Function} [options.onChange] - * Optional callback fired whenever the attendance map changes (toggle or mark-all). + * @param {Function} [options.onChange] - Optional callback fired whenever the attendance map + * changes (toggle or mark-all). + * @returns {object} Attendance form state and methods. */ export default function useAttendanceForm({ hasChanges, markClean, submitting, onChange }) { const router = useRouter(); diff --git a/kolibri/plugins/coach/frontend/composables/useCoachTabs.js b/kolibri/plugins/coach/frontend/composables/useCoachTabs.js index 4bbbda090d4..099dddbc89f 100644 --- a/kolibri/plugins/coach/frontend/composables/useCoachTabs.js +++ b/kolibri/plugins/coach/frontend/composables/useCoachTabs.js @@ -44,9 +44,8 @@ const lastTabsInteraction = reactive({ export function useCoachTabs() { /** * Stores an interaction with tabs - * - * @param {String} tabsInterfaceId ID of a tabbed interface interacted with - * @param {String} event An event kind. Available kinds: 'click' + * @param {string} tabsInterfaceId - ID of a tabbed interface interacted with + * @param {string} event - An event kind. Available kinds: 'click' */ function saveTabsInteraction(tabsInterfaceId, event) { lastTabsInteraction.tabsInterfaceId = tabsInterfaceId; @@ -56,16 +55,16 @@ export function useCoachTabs() { /** * Stores a click interaction with tabs - * - * @param {String} tabsInterfaceId ID of a tabbed interface interacted with + * @param {string} tabsInterfaceId - ID of a tabbed interface interacted with */ function saveTabsClick(tabsInterfaceId) { saveTabsInteraction(tabsInterfaceId, TabsEvents.CLICK); } /** - * @param {String} tabsInterfaceId ID of a tabbed interface - * @returns {Boolean} `true` when tabs with the provided ID were + * Returns whether the given tabs interface was clicked recently + * @param {string} tabsInterfaceId - ID of a tabbed interface + * @returns {boolean} `true` when tabs with the provided ID were * clicked recently */ function wereTabsClickedRecently(tabsInterfaceId) { diff --git a/kolibri/plugins/coach/frontend/composables/useCourseSession.js b/kolibri/plugins/coach/frontend/composables/useCourseSession.js index df51d6c76a7..cb86016be41 100644 --- a/kolibri/plugins/coach/frontend/composables/useCourseSession.js +++ b/kolibri/plugins/coach/frontend/composables/useCourseSession.js @@ -22,9 +22,8 @@ const { defaultErrorMessage$ } = coreStrings; * A composable for managing course session state. * Handles fetching course session data, course content, active tests, and test history. * Provides derived state for units and unit phase. - * - * @param {Ref} courseSessionId - The ID of the course session to load - * @returns {Object} Reactive state and methods for managing the course session + * @param {object} courseSessionId - A ref containing the course session ID + * @returns {object} Reactive state and methods for managing the course session */ export default function useCourseSession(courseSessionId) { const { createSnackbar } = useSnackbar(); @@ -177,7 +176,6 @@ export default function useCourseSession(courseSessionId) { /** * Activates a test for the current active unit. - * * @param {string} testType - Either 'pre' or 'post' * @returns {Promise} Resolves when the test is activated */ @@ -209,7 +207,6 @@ export default function useCourseSession(courseSessionId) { /** * Closes the currently active test. * Moves the closed test to history and clears activeTest. - * * @returns {Promise} Resolves when the test is closed */ function closeTest() { @@ -244,7 +241,6 @@ export default function useCourseSession(courseSessionId) { /** * Toggles the active state of the course session. * Updates the courseSession ref with the new state on success. - * * @returns {Promise} Resolves with the updated course session */ function toggleCourseActive() { diff --git a/kolibri/plugins/coach/frontend/composables/useFetchTree.js b/kolibri/plugins/coach/frontend/composables/useFetchTree.js index 3cf4fa3dbc8..2efbde77017 100644 --- a/kolibri/plugins/coach/frontend/composables/useFetchTree.js +++ b/kolibri/plugins/coach/frontend/composables/useFetchTree.js @@ -3,55 +3,61 @@ import { computed, ref } from 'vue'; import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource'; /** - * @deftype FetchTreeConfig + * @typedef {import('kolibri-common/apiResources/ContentNodeResource').ContentNode} ContentNode + */ + +/** + * @typedef {object} FetchTreeConfig * @property {computed} topicId - The id of the root node to fetch the - * children for - * @property {Object} [params] - Params to pass to the ContentNodeResource.fetchTree method this - * can include any keys that our API supports for filtering the results. - * Example: { kind_in: [ContentNodeKinds.EXERCISE, ContentNodeKinds.TOPIC] } - * Default: {} + * children for. + * @property {object} [params] - Params to pass to the `ContentNodeResource.fetchTree` method; + * this can include any keys that our API supports for filtering the results. Example: + * `{kind_in: [ContentNodeKinds.EXERCISE, ContentNodeKinds.TOPIC]}`. Default: `{}`. */ /** + * An API wrapper for fetching contents by way of the `ContentNodeResource.fetchTree` method + * with specific helper methods for lazily loading more of the contents within a topic. * @module useFetchTree - * @description An API wrapper for fetching contents by way of the ContentNodeResource.fetchTree - * method with specific helper methods for lazily loading more of the contents within a topic. + * @param {FetchTreeConfig} config - Fetch tree configuration. + * @returns {object} Reactive topic, resources, loading state, and fetch methods. */ export default function useFetchTree({ topicId, params = {} } = {}) { - /** Private variables: - * Note that the _ prefix is used here to indicate that these are values that should only be able - * to be mutated within this module and are read-only outside of it. To make changes to these - * values, use the methods provided by this module (or add a method, if needed). */ + // Private variables + // + // Note that the _ prefix is used here to indicate that these are values that should only + // be able to be mutated within this module and are read-only outside of it. To make + // changes to these values, use the methods provided by this module (or add a method, if + // needed). /** @type {ref} The topic node that we are fetching the children for */ const _topic = ref(null); - /** @type {ref} All resources which have been fetched */ + /** @type {ref} All resources which have been fetched */ const _resources = ref([]); - /** @type {ref} Whether we are currently fetching/processing the child nodes */ + /** @type {ref} Whether we are currently fetching/processing the child nodes */ const _loading = ref(false); - /** @type {ref} The params object we pass to the ContentNodeResource fetchTree method - * to fetch the next batch of nodes. Note that the `more` we get from the API will include the - * same parameters as we sent in the first call. - * When null, there are no more to fetch for the current topicId */ + /** + * @type {ref} The params object we pass to the `ContentNodeResource.fetchTree` + * method to fetch the next batch of nodes. Note that the `more` we get from the API will + * include the same parameters as we sent in the first call. When null, there are no more + * to fetch for the current topicId. + */ const _moreParams = ref(null); const hasMore = computed(() => get(_moreParams) !== null); /** - * @description TODO - * - * @param {Object} params - Params to pass to the ContentNodeResource fetchTree method - * (Default: {}) - * - * @affects _resources - The list of resources will be updated with the new list of _resource - * @affects _loading - The loading state will be set to true while the fetch is in progress and - * then set to false when it completes + * Fetches a batch of content nodes under the current topic. + * @param {object} params - Params to pass to the `ContentNodeResource.fetchTree` method + * (default: `{}`). + * @returns {Promise} Resolves with the list of resources fetched. + * @affects _resources - The list of resources will be updated with the new list of resources. + * @affects _loading - The loading state will be set to true while the fetch is in progress + * and then set to false when it completes. * @affects _moreParams - * - * @returns {Promise} A promise that resolves to the list of resources fetched - **/ + */ async function _fetchNodeTree(params) { set(_loading, true); @@ -74,8 +80,11 @@ export default function useFetchTree({ topicId, params = {} } = {}) { return _fetchNodeTree(params); } - /** Fetches the next batch of nodes, which fetchTree will do on its own, but this makes for a - * easier-to-understand API */ + /** + * Fetches the next batch of nodes, which fetchTree will do on its own, but this makes + * for an easier-to-understand API. + * @returns {Promise} Resolves with the next batch of resources. + */ async function fetchMore() { if (!get(hasMore)) { return Promise.reject('Tried to call fetchMore when no more ContentNodes are available'); diff --git a/kolibri/plugins/coach/frontend/composables/useQuizCreation.js b/kolibri/plugins/coach/frontend/composables/useQuizCreation.js index 12c37c4f8d2..ece9c140289 100644 --- a/kolibri/plugins/coach/frontend/composables/useQuizCreation.js +++ b/kolibri/plugins/coach/frontend/composables/useQuizCreation.js @@ -11,7 +11,15 @@ import { fetchExamWithContent } from 'kolibri-common/quizzes/utils'; import selectQuestions, { exerciseToQuestionArray } from '../utils/selectQuestions.js'; import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js'; -/** Validators **/ +/** + * @typedef {import('vue').ComputedRef} ComputedRef + * @typedef {import('./quizCreationSpecs.js').Quiz} Quiz + * @typedef {import('./quizCreationSpecs.js').QuizSection} QuizSection + * @typedef {import('./quizCreationSpecs.js').QuizQuestion} QuizQuestion + * @typedef {import('./quizCreationSpecs.js').QuizExercise} QuizExercise + */ + +// 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 * of the properties. */ @@ -33,7 +41,8 @@ const fieldsToSave = [ ]; /** - * Composable function presenting primary interface for Quiz Creation + * Composable providing quiz creation state and actions. + * @returns {object} Quiz creation state and action methods. */ export default function useQuizCreation() { const store = getCurrentInstance()?.proxy?.$store; @@ -43,17 +52,19 @@ export default function useQuizCreation() { const quizHasChanged = ref(false); - /** @type {ref} - * The "source of truth" quiz object from which all reactive properties should derive - * This will be validated and sent to the API when the user saves the quiz */ + /** + * @type {ref} The "source of truth" quiz object from which all reactive properties + * should derive. This will be validated and sent to the API when the user saves the quiz. + */ const _quiz = ref(objectWithDefaults({}, Quiz)); - /** @type {ref} - * The section that is currently selected for editing */ + /** @type {ref} The section that is currently selected for editing */ const activeSectionIndex = computed(() => Number(store?.state?.route?.params?.sectionIndex || 0)); - /** @type {ref} - * The QuizQuestion.items that are currently selected for action in the active section */ + /** + * @type {ref} The `QuizQuestion.items` that are currently selected for action in + * the active section. + */ const _selectedQuestionIds = ref([]); // An internal map for exercises @@ -77,11 +88,11 @@ export default function useQuizCreation() { // ------------------ /** - * @param {QuizSection} section - * @returns {QuizSection} - * @affects _quiz - Updates the section with the given sectionIndex with the given param - * @throws {TypeError} if section is not a valid QuizSection - **/ + * Update the section with the given sectionIndex with the given param. + * @param {QuizSection} section - The section shape to merge, keyed by `sectionIndex`. + * @throws {TypeError} If `section` is not a valid QuizSection. + * @affects _quiz - Updates the section with the given sectionIndex with the given param. + */ function updateSection({ sectionIndex, ...updates }) { set(quizHasChanged, true); const targetSection = get(allSections)[sectionIndex]; @@ -156,12 +167,12 @@ export default function useQuizCreation() { } /** - * Replace `questionItemsToReplace` questions in the `baseQuestions` array with the - * `replacements` questions - * @param {Array} baseQuestions base questions array - * @param {Array} questionItemsToReplace question items to replace - * @param {Array} replacements array of questions to replace the question items - * @returns + * Replace selected questions in `baseQuestions` with new questions, keeping order. + * @param {Array} baseQuestions - The original questions array. + * @param {Array} questionItemsToReplace - Question item IDs that should be swapped out. + * @param {Array} replacements - The new questions to take their place. + * @returns {Array} The new questions array with replacements applied. + * @throws {TypeError} If `replacements.length` does not equal `questionItemsToReplace.length`. */ function _replaceQuestions(baseQuestions, questionItemsToReplace, replacements) { if (questionItemsToReplace.length !== replacements.length) { @@ -179,11 +190,14 @@ export default function useQuizCreation() { } /** - * Add an array of questions to a section - * @param {Object} options - * @param {number} options.sectionIndex - The index of the section to add the questions to - * @param {QuizQuestion[]} options.questions - The questions array to add - * @param {QuizExercise[]} options.resources - The resources to add to the exercise map + * Add an array of questions to a section, optionally replacing existing items. + * @param {object} options - Options object. + * @param {number} options.sectionIndex - Where in `_quiz.question_sources` to add questions. + * @param {QuizQuestion[]} options.questions - The questions array to add. + * @param {QuizExercise[]} options.resources - The resources to add to the exercise map. + * @param {string[]} [options.questionItemsToReplace] - Question items to replace instead of + * appending. + * @throws {TypeError} If the section is not found or `questions` is empty. */ function addQuestionsToSection({ sectionIndex, questions, resources, questionItemsToReplace }) { const targetSection = get(allSections)[sectionIndex]; @@ -213,8 +227,10 @@ export default function useQuizCreation() { updateSection({ sectionIndex, questions: questionsToAdd, resourcePool: resources }); } - /** @returns {QuizSection} - * Adds a section to the quiz and returns it */ + /** + * Adds a new empty section to the quiz. + * @returns {object} The newly created quiz section. + */ function addSection() { const newSection = objectWithDefaults({}, QuizSection); updateQuiz({ question_sources: [...get(quiz).question_sources, newSection] }); @@ -222,8 +238,10 @@ export default function useQuizCreation() { } /** - * @throws {Error} if section not found - * Deletes the given section by sectionIndex */ + * Deletes the given section by sectionIndex. + * @param {number} sectionIndex - The index of the section to remove. + * @throws {Error} If section not found. + */ function removeSection(sectionIndex) { if (!get(allSections)[sectionIndex]) { throw new Error(`Section with index ${sectionIndex} not found; cannot be removed.`); @@ -246,12 +264,12 @@ export default function useQuizCreation() { // Quiz General // ------------ - /** @affects _quiz - * @affects activeSectionIndex - * @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 - * use */ - + /** + * Initializes the quiz state, either creating a new quiz or loading an existing one. + * @param {string} collection - The collection ID to assign the quiz to. + * @param {string} quizId - The ID of an existing quiz to load, or 'new' to create a new quiz. + * @returns {Promise} Resolves when initialization is complete. + */ async function initializeQuiz(collection, quizId = 'new') { if (quizId === 'new') { const assignments = [collection]; @@ -273,7 +291,8 @@ export default function useQuizCreation() { } /** - * @returns {Promise} + * Saves the current quiz state to the server. + * @returns {Promise} Resolves with the saved exam object. */ function saveQuiz() { if (!validateQuiz(get(_quiz))) { @@ -315,10 +334,11 @@ export default function useQuizCreation() { } /** - * @param {Quiz} updates - * @throws {TypeError} if updates is not a valid Quiz object + * Validates the input type and then updates `_quiz` with the given updates. + * @param {Quiz} updates - The partial quiz update to apply. + * @throws {TypeError} If `updates` is not a valid Quiz object. * @affects _quiz - * Validates the input type and then updates _quiz with the given updates */ + */ function updateQuiz(updates) { set(quizHasChanged, true); if (!validateQuiz(updates)) { @@ -331,16 +351,20 @@ export default function useQuizCreation() { // Questions / Exercises management // -------------------------------- - /** @param {QuizQuestion[]} questions - * @affects _selectedQuestionIds - Adds question to _selectedQuestionIds if it isn't - * there already */ + /** + * Adds question IDs to the current selection, deduplicating as needed. + * @param {Array} ids - Array of question item IDs to add to the selection. + * @returns {void} + */ function addQuestionsToSelection(ids) { set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), ...ids])); } /** - * @param {QuizQuestion[]} questions - * @affects _selectedQuestionIds - Removes question from _selectedQuestionIds if it is there */ + * Removes question IDs from the current selection. + * @param {Array} ids - Array of question item IDs to remove from the selection. + * @returns {void} + */ function removeQuestionsFromSelection(ids) { set( _selectedQuestionIds, @@ -355,25 +379,29 @@ export default function useQuizCreation() { // Utilities // Computed properties - /** @type {ComputedRef} The value of _quiz */ + /** @type {ComputedRef} The value of `_quiz` */ const quiz = computed(() => get(_quiz)); - /** @type {ComputedRef} The value of _quiz's `question_sources` */ + /** @type {ComputedRef} The value of `_quiz.question_sources` */ const allSections = computed(() => get(quiz).question_sources); - /** @type {ComputedRef} The active section */ + /** @type {ComputedRef} The currently selected section, by `activeSectionIndex` */ const activeSection = computed(() => get(allSections)[get(activeSectionIndex)]); - /** @type {ComputedRef} The inactive sections */ + /** @type {ComputedRef} All sections except the active one */ const inactiveSections = computed(() => get(allSections) .slice(0, get(activeSectionIndex)) .concat(get(allSections).slice(get(activeSectionIndex) + 1)), ); - /** @type {ComputedRef} All questions in the active section's `questions` property - * those which are currently set to be used in the section */ + /** + * @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>} - * A map of exercise id to exercise for the currently active section */ + /** + * @type {ComputedRef<{[key: string]: QuizExercise}>} A map of exercise id to exercise for + * the currently active section. + */ const activeResourceMap = computed(() => { const map = {}; for (const question of get(activeQuestions)) { @@ -390,11 +418,13 @@ export default function useQuizCreation() { return _quiz.value && _exerciseMap; }); - /** @type {ComputedRef} The active section's exercises */ + /** @type {ComputedRef} The active section's exercises */ const activeResourcePool = computed(() => Object.values(get(activeResourceMap))); - /** @type {ComputedRef} - * All QuizQuestion.items the user selected for the active section */ + /** + * @type {ComputedRef} All `QuizQuestion.items` the user selected for the active + * section. + */ const selectedActiveQuestions = computed(() => get(_selectedQuestionIds)); /** @type {ComputedRef>} A list of all questions in the quiz */ @@ -410,7 +440,8 @@ export default function useQuizCreation() { }); /** - + * Removes all currently selected questions from the active section. + * @returns {void} */ function deleteActiveSelectedQuestions() { const sectionIndex = get(activeSectionIndex); @@ -425,7 +456,9 @@ export default function useQuizCreation() { } const noQuestionsSelected = computed(() => get(selectedActiveQuestions).length === 0); - /** @type {ComputedRef} The label that should be shown alongside the "Select all" checkbox + /** + * @type {ComputedRef} The label that should be shown alongside the "Select all" + * checkbox. */ const selectAllLabel = computed(() => { if (get(noQuestionsSelected)) { @@ -438,8 +471,8 @@ export default function useQuizCreation() { }); /** - * Map of exercise id to array of question items that are not used for each exercise - * @type {ComputedRef>} + * Map of exercise id to array of question items that are not used for each exercise. + * @type {ComputedRef<{[key: string]: string[]}>} */ const activeExercisesUnusedQuestionsMap = computed(() => { const map = {}; @@ -454,10 +487,10 @@ export default function useQuizCreation() { }); /** - * Method to replace questions in `questionItems` with new questions selected from - * the unused questions of the exercises that each question belongs to. - * @param {Array} questionItems - * @throws {Error} If there are no enough unused questions in the exercise to replace a question + * Replace questions in `questionItems` with new questions from the unused questions of each + * question's exercise. + * @param {Array} questionItems - Question item IDs to replace. + * @throws {Error} If there are not enough unused questions in the exercise to replace a question. */ function autoReplaceQuestions(questionItems = []) { if (!questionItems?.length) { diff --git a/kolibri/plugins/coach/frontend/composables/useQuizResources.js b/kolibri/plugins/coach/frontend/composables/useQuizResources.js index afeccb47a00..f91d4c837ae 100644 --- a/kolibri/plugins/coach/frontend/composables/useQuizResources.js +++ b/kolibri/plugins/coach/frontend/composables/useQuizResources.js @@ -5,17 +5,24 @@ import { ContentNodeKinds } from 'kolibri/constants'; import logging from 'kolibri-logging'; import useFetchTree from './useFetchTree'; +/** + * @typedef {import('kolibri-common/apiResources/ContentNodeResource').ContentNode} ContentNode + * @typedef {ContentNode} ExerciseResource + */ + const logger = logging.getLogger(__filename); const _loadingMore = ref(false); /** - * @typedef {Object} QuizResourcesConfig + * @typedef {object} QuizResourcesConfig * @property { computed } topicId - The id of the root node to fetch the - * children for + * children for */ /** + * Composable for fetching and managing quiz-eligible resources from a content topic. * @module useQuizResources - * @param {QuizResourcesConfig} config + * @param {QuizResourcesConfig} config - Configuration object. + * @returns {object} Resource list, loading state, and fetch methods. */ export default function useQuizResources({ topicId, practiceQuiz = false } = {}) { const params = { @@ -39,11 +46,13 @@ export default function useQuizResources({ topicId, practiceQuiz = false } = {}) params, }); - /** @type {ref} All resources which have been fetched that are the children of - * the given topicId annotated with assessment metadata */ + /** + * @type {ref} All resources which have been fetched that are the children of + * the given topicId annotated with assessment metadata + */ const _resources = ref([]); - /** @type {ref} Whether we are currently fetching/processing the child nodes */ + /** @type {ref} Whether we are currently fetching/processing the child nodes */ const _loading = ref(false); /** @@ -97,9 +106,10 @@ export default function useQuizResources({ topicId, practiceQuiz = false } = {}) } /** - * @affects _resources - Sets the _resources to the results of the fetchTree call - * @affects _loading - * @returns {Promise} - A promise that resolves when the annotations have been made and + * Fetches the initial set of quiz resources for the current topic. + * @returns {Promise} Resolves when resources have been fetched and annotated. + * @affects _resources - Sets the _resources to the results of the fetchTree call + * @affects _loading */ async function fetchQuizResources() { set(_loading, true); @@ -112,9 +122,11 @@ export default function useQuizResources({ topicId, practiceQuiz = false } = {}) } /** - * @affects _resources - Appends the results of the fetchMore call to the _resources - * and annotates any new topics with descendant counts - * @affects _loading - fetchMore & annotateTopicsWithDescendantCounts update the loading states + * Fetches the next page of quiz resources and appends them to the current list. + * @returns {Promise} Resolves when additional resources have been fetched and annotated. + * @affects _resources - Appends the results of the fetchMore call to the _resources + * and annotates any new topics with descendant counts + * @affects _loading - fetchMore & annotateTopicsWithDescendantCounts update the loading states */ async function fetchMoreQuizResources() { set(_loading, true); diff --git a/kolibri/plugins/coach/frontend/composables/useResourceSelection.js b/kolibri/plugins/coach/frontend/composables/useResourceSelection.js index 9b62d90489a..af2963f11b4 100644 --- a/kolibri/plugins/coach/frontend/composables/useResourceSelection.js +++ b/kolibri/plugins/coach/frontend/composables/useResourceSelection.js @@ -11,53 +11,56 @@ import useFetch from 'kolibri-common/composables/useFetch.js'; */ /** - * Composable for managing the selection of resources within a topic tree. - * This utility handles selection rules, manages fetch states for channels, bookmarks, - * 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. - * @param {Object} options.search Configuration object for search fetch. It can contain - * `filters` an object with extra query params that will be present in all search requests. - * - * @typedef {Object} UseResourceSelectionResponse - * @property {Object} topic Topic tree object, contains the information of the topic, - * its ascendants and children. - * Defined only if the `topicId` query in the route is set. - * @property {boolean} loading Indicates whether the main topic tree, channels, and bookmarks + * @typedef {object} UseResourceSelectionResponse + * @property {object} topic - Topic tree object; contains the information of the topic, its + * ascendants and children. Defined only if the `topicId` query in the route is set. + * @property {boolean} loading - Indicates whether the main topic tree, channels, and bookmarks * data are currently loading. This does not account for loading more data. For such cases, * use the fetch objects of each entity. - * @property {FetchObject} channelsFetch Channels fetch object to manage the process of + * @property {FetchObject} channelsFetch - Channels fetch object to manage the process of * fetching channels. We currently don't support fetching more channels. - * @property {FetchObject} bookmarksFetch Bookmarks fetch object to manage the process of + * @property {FetchObject} bookmarksFetch - Bookmarks fetch object to manage the process of * fetching bookmarks. Fetching more bookmarks is supported. - * @property {FetchObject} treeFetch Topic tree fetch object to manage the process of + * @property {FetchObject} treeFetch - Topic tree fetch object to manage the process of * fetching topic trees and their resources. Fetching more resources is supported. - * @property {FetchObject} searchFetch Search fetch object to manage the process of - * fetching search results. Fetching more search results is supported. - * @property {Array} searchTerms The search terms used to filter the search results. - * @property {boolean} displayingSearchResults Indicates whether we currently have search terms. - * @property {Array<(node: Object) => boolean>} selectionRules An array of functions that determine - * whether a node can be selected. - * @property {Array} selectedResources An array of currently selected resources. - * @property {(resources: Array) => void} selectResources Adds the specified resources + * @property {FetchObject} searchFetch - Search fetch object to manage the process of fetching + * search results. Fetching more search results is supported. + * @property {Array} searchTerms - The search terms used to filter the search results. + * @property {boolean} displayingSearchResults - Indicates whether we currently have search + * terms. + * @property {Array<(node: object) => boolean>} selectionRules - An array of functions that + * determine whether a node can be selected. + * @property {Array} selectedResources - An array of currently selected resources. + * @property {(resources: Array) => void} selectResources - Adds the specified resources * to the `selectedResources` array. - * @property {(resources: Array) => void} deselectResources Removes the specified resources - * from the `selectedResources` array. - * @property {(resources: Array) => void} setSelectedResources Replaces the current + * @property {(resources: Array) => void} deselectResources - Removes the specified + * resources from the `selectedResources` array. + * @property {(resources: Array) => void} setSelectedResources - Replaces the current * `selectedResources` array with the provided resources array. - * @property {() => void} clearSearch Clears the current search terms and results. - * @property {(tag: Object) => void} removeSearchFilterTag Removes the specified tag from the - * search terms. - * - * @returns {UseResourceSelectionResponse} + * @property {() => void} clearSearch - Clears the current search terms and results. + * @property {(tag: object) => void} removeSearchFilterTag - Removes the specified tag from the + * search terms. + */ + +/** + * Composable for managing the selection of resources within a topic tree. + * This utility handles selection rules, manages fetch states for channels, bookmarks, + * and topic trees, and offers methods to add, remove, or override selected resources. + * @param {object} options - Options object. + * @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. + * @param {object} options.search - Configuration object for search fetch. It can contain + * `filters` an object with extra query params that will be present in all search requests. + * @returns {UseResourceSelectionResponse} Resource selection state and methods. */ export default function useResourceSelection({ searchResultsRouteName, diff --git a/kolibri/plugins/coach/frontend/constants/index.js b/kolibri/plugins/coach/frontend/constants/index.js index 7c8bed2e7b7..21ad1c5f869 100644 --- a/kolibri/plugins/coach/frontend/constants/index.js +++ b/kolibri/plugins/coach/frontend/constants/index.js @@ -1,9 +1,9 @@ export const PageNames = { - /** Class Summary */ + /* Class Summary */ HOME_PAGE: 'HomePage', // make sure this matches the Coach 'Home' page name EXAM_CREATION_ROOT: 'EXAM_CREATION_ROOT', - /** Exams and quizzes */ + /* Exams and quizzes */ EXAMS_ROOT: 'EXAMS_ROOT', EXAM_SUMMARY: 'EXAM_SUMMARY', QUIZ_PREVIEW: 'QUIZ_PREVIEW', @@ -61,7 +61,7 @@ export const PageNames = { LEARNER_LESSON_REPORT: 'LEARNER_LESSON_REPORT', LEARNER_PASSWORDS: 'LEARNER_PASSWORDS', - /** Groups */ + /* Groups */ GROUPS_ROOT: 'GROUPS_ROOT', GROUP_SUMMARY: 'GROUP_SUMMARY', GROUP_ENROLL: 'GROUP_ENROLL', @@ -71,7 +71,7 @@ export const PageNames = { GROUP_LESSON_EXERCISE_QUESTIONS_REPORT: 'GROUP_LESSON_EXERCISE_QUESTIONS_REPORT', GROUP_LESSON_EXERCISE_LEARNER_REPORT: 'GROUP_LESSON_EXERCISE_LEARNER_REPORT', - /** Courses */ + /* Courses */ COURSES_ROOT: 'COURSES_ROOT', COURSE_SUMMARY: 'COURSE_SUMMARY', COURSES_ASSIGN: 'COURSES_ASSIGN', @@ -80,7 +80,7 @@ export const PageNames = { COURSES_ASSIGN_SELECT_RECIPIENTS: 'COURSES_ASSIGN_SELECT_RECIPIENTS', COURSES_ASSIGN_PREVIEW_LEARNERS: 'COURSES_ASSIGN_PREVIEW_LEARNERS', - /** Attendance */ + /* Attendance */ ATTENDANCE_NEW: 'ATTENDANCE_NEW', ATTENDANCE_HISTORY: 'ATTENDANCE_HISTORY', ATTENDANCE_EDIT: 'ATTENDANCE_EDIT', diff --git a/kolibri/plugins/coach/frontend/csv/exporter.js b/kolibri/plugins/coach/frontend/csv/exporter.js index aeb77740898..cf02ae23e64 100644 --- a/kolibri/plugins/coach/frontend/csv/exporter.js +++ b/kolibri/plugins/coach/frontend/csv/exporter.js @@ -11,11 +11,13 @@ const NAME_DEFAULTS = { class CSVExporter { /** - * @param {Object[]} columns - * @param {String} columns[].name The title of the column - * @param {String} columns[].key The key of the column - * @param {Function} [columns[].format] A function that will produce the value for a row - * @param {String} [baseFilename] + * Construct a CSV exporter for a fixed set of columns. + * @param {object[]} columns - Column descriptors, in output order. + * @param {string} columns[].name - The title of the column. + * @param {string} columns[].key - The key of the column (supports `dot.notation` for + * nested fields). + * @param {Function} [columns[].format] - A function that will produce the value for a row. + * @param {string} [baseFilename] - Optional prefix for the generated filename. */ constructor(columns, baseFilename = '') { this._columns = columns; @@ -26,7 +28,8 @@ class CSVExporter { } /** - * @param {Object} names + * Merge `names` into the names used to build the output filename. + * @param {object} names - Map of name parts (e.g. `{lesson, resource, group, learner}`). */ addNames(names) { this._names = { @@ -36,7 +39,8 @@ class CSVExporter { } /** - * @return {String} + * Compose the sanitised CSV filename from the configured base and name parts. + * @returns {string} The filename, including the `.csv` extension. */ buildFilename() { const filenameParts = [this._filename]; @@ -70,8 +74,11 @@ class CSVExporter { } /** - * @param {Object[]} dataArray - * @return {mixed[]} + * Apply each column's `format` function (or extract the value at `column.key`) to + * every row, prepending a header row of column names. + * @param {object[]} dataArray - Rows of source data. + * @returns {Array>} Two-dimensional array suitable for CSV generation, + * with column names as the first row. */ formatData(dataArray) { return [ @@ -90,7 +97,8 @@ class CSVExporter { } /** - * @param {Object[]} dataArray + * Trigger a CSV download in the browser for the given rows. + * @param {object[]} dataArray - Rows of source data, formatted using `formatData`. */ export(dataArray) { csvGenerator.download({ diff --git a/kolibri/plugins/coach/frontend/csv/fields.js b/kolibri/plugins/coach/frontend/csv/fields.js index f766c0859cb..28f9799c514 100644 --- a/kolibri/plugins/coach/frontend/csv/fields.js +++ b/kolibri/plugins/coach/frontend/csv/fields.js @@ -56,8 +56,9 @@ const VERB_MAP = { */ /** - * @param {String|Number} interval - * @return {string} + * Pads a time interval value with leading zeros to at least 2 digits. + * @param {number|string} interval - The time value to pad. + * @returns {string} The zero-padded string. */ function padTime(interval) { return pad(interval, 2, '0'); diff --git a/kolibri/plugins/coach/frontend/modules/pluginModule.js b/kolibri/plugins/coach/frontend/modules/pluginModule.js index 8bc1230bc8c..1770e342e15 100644 --- a/kolibri/plugins/coach/frontend/modules/pluginModule.js +++ b/kolibri/plugins/coach/frontend/modules/pluginModule.js @@ -98,10 +98,12 @@ export default { }); }, /** - * Handle coach page errors. - * The status code errors that's related to the authentication issue, most not show + * Handle coach page errors. + * The status code errors that's related to the authentication issue, most not show in coach page beacuse there's an `auth-message` that explain the error. - **/ + * @param {object} store - The Vuex store action context. + * @param {object} errorObject - The error caught from a failed coach page request. + */ handleCoachPageError(store, errorObject) { const authErrorCodes = [401, 403, 404, 407]; logging.error(errorObject); diff --git a/kolibri/plugins/coach/frontend/safeFilename.js b/kolibri/plugins/coach/frontend/safeFilename.js index 96a7ca05cf5..3b5e3a656bb 100644 --- a/kolibri/plugins/coach/frontend/safeFilename.js +++ b/kolibri/plugins/coach/frontend/safeFilename.js @@ -47,9 +47,13 @@ function sanitize(input, replacement) { } /** - * @param {String} input Original filename - * @param {Object} [options] {replacement: String | Function } - * @return {String} Sanitized filename + * Sanitize a filename, removing characters that are unsafe on common operating + * systems and capping the result at 255 bytes. Pass a replacement string to + * substitute unsafe characters; characters that remain unsafe after substitution + * are then stripped. + * @param {string} input - Original filename. + * @param {{replacement?: string|Function}} [options] - Sanitisation options. + * @returns {string} Sanitised filename, safe to write on macOS, Linux, and Windows. */ export default function (input, options) { const replacement = (options && options.replacement) || ''; diff --git a/kolibri/plugins/coach/frontend/utils/index.js b/kolibri/plugins/coach/frontend/utils/index.js index 042ace34c4c..1520dfc0a8b 100644 --- a/kolibri/plugins/coach/frontend/utils/index.js +++ b/kolibri/plugins/coach/frontend/utils/index.js @@ -27,7 +27,10 @@ export const getDifficultQuestions = questions => { }; /** - * Override the route with a new one, preserving the params and query + * Merges params and query from a new route into the current route, overriding existing values. + * @param {object} route - The current route object with params and query. + * @param {object} newRoute - The new route object to merge params and query from. + * @returns {object} A new route object with merged params and query. */ export function overrideRoute(route, newRoute) { const { params, query } = route; diff --git a/kolibri/plugins/coach/frontend/utils/scoreBucketing.js b/kolibri/plugins/coach/frontend/utils/scoreBucketing.js index 4a814531c6e..b6f98a4e42e 100644 --- a/kolibri/plugins/coach/frontend/utils/scoreBucketing.js +++ b/kolibri/plugins/coach/frontend/utils/scoreBucketing.js @@ -3,10 +3,9 @@ import { MasteryThreshold, ScoreBucket } from '../constants/courseConstants'; /** * Classifies a learner's mastery for a learning objective based on their * correct count and total questions. - * - * @param {number} correctCount - number of questions answered correctly - * @param {number} numQuestions - total questions for this LO - * @returns {string} ScoreBucket.LOW, ScoreBucket.MID, or ScoreBucket.HIGH + * @param {number} correctCount - Number of questions answered correctly. + * @param {number} numQuestions - Total questions for this LO. + * @returns {string} ScoreBucket.LOW, ScoreBucket.MID, or ScoreBucket.HIGH. */ export function classifyLearnerMastery(correctCount, numQuestions) { const ratio = numQuestions > 0 ? correctCount / numQuestions : 0; @@ -17,11 +16,11 @@ export function classifyLearnerMastery(correctCount, numQuestions) { /** * Buckets learner scores for a single learning objective into low/mid/high counts. - * - * @param {Object} scores - { learnerId: { loId: correctCount, ... }, ... } - * @param {string} loId - the learning objective ID - * @param {number} numQuestions - total questions for this LO - * @returns {{ lowCount: number, midCount: number, highCount: number }} + * @param {object} scores - Map of learner IDs to their correct counts, keyed by learning + * objective ID. + * @param {string} loId - The learning objective ID to bucket scores for. + * @param {number} numQuestions - Total number of questions for the learning objective. + * @returns {{lowCount: number, midCount: number, highCount: number}} Counts per bucket. */ export function bucketScoresForObjective(scores, loId, numQuestions) { let lowCount = 0; @@ -46,11 +45,11 @@ export function bucketScoresForObjective(scores, loId, numQuestions) { /** * Maps over an array of learning objectives and returns bucketed results for each. - * - * @param {Array<{ id: string, text: string, num_questions: number }>} learningObjectives - * @param {Object} scores - { learnerId: { loId: correctCount, ... }, ... } - * @returns {Array} Array of { id, text, numQuestions, - * lowCount, midCount, highCount } + * @param {Array<{id: string, text: string, num_questions: number}>} learningObjectives - The + * learning objectives to bucket. + * @param {object} scores - Map of learner IDs to their correct counts, keyed by learning + * objective ID. + * @returns {Array} Array of `{id, text, numQuestions, lowCount, midCount, highCount}`. */ export function bucketAllObjectives(learningObjectives, scores) { return learningObjectives.map(lo => { @@ -71,12 +70,11 @@ export function bucketAllObjectives(learningObjectives, scores) { } /** - * Given raw unit report API data, determines the active test and buckets - * the learning objectives by learner mastery. - * - * @param {Object} reportData - raw response from UnitReportResource.fetchReport - * @returns {{ activeTestType: string|null, activeTestStatus: string, - * bucketedObjectives: Array }} + * Given raw unit report API data, determines the active test and buckets the learning + * objectives by learner mastery. + * @param {object} reportData - Raw response from `UnitReportResource.fetchReport`. + * @returns {{activeTestType: string|null, activeTestStatus: string, + * bucketedObjectives: Array}} Derived unit report summary. */ export function deriveUnitReportInfo(reportData) { const { post_test, pre_test, learning_objectives } = reportData; diff --git a/kolibri/plugins/coach/frontend/utils/selectQuestions.js b/kolibri/plugins/coach/frontend/utils/selectQuestions.js index a2869e9e1f8..aeb85e6ce42 100644 --- a/kolibri/plugins/coach/frontend/utils/selectQuestions.js +++ b/kolibri/plugins/coach/frontend/utils/selectQuestions.js @@ -42,15 +42,12 @@ function getExerciseQuestionsMap(exercises, excludedQuestionIds = []) { } /** - * Choose a an evenly-distributed random selection of questions from exercises. Note that the order - * of the arrays should correspond to each other, ie, exerciseIds[i] should correspond to - * questionIdArrays[i] should correspond to exerciseTitles[i], etc. - * - * @param {Number} numQuestions - target number of questions - * @param {String[]} exercises - Exercise objects - * @param {number} seed - value to seed the random shuffle with - * - * @return {QuizQuestion[]} + * Selects a set of questions from the given exercises using a seeded shuffle. + * @param {number} numQuestions - The number of questions to select. + * @param {Array} exercises - Array of exercise content node objects to draw questions from. + * @param {number} seed - Seed value for the random shuffle. + * @param {Array} excludedQuestionIds - Question item IDs to exclude from selection. + * @returns {Array} Array of selected question objects sorted by title. */ export default function selectQuestions(numQuestions, exercises, seed, excludedQuestionIds = []) { const allQuestionsByExercise = getExerciseQuestionsMap(exercises, excludedQuestionIds); diff --git a/kolibri/plugins/coach/frontend/views/attendance/__tests__/AttendancePages.spec.js b/kolibri/plugins/coach/frontend/views/attendance/__tests__/AttendancePages.spec.js index cc69fdcdfef..164822b9324 100644 --- a/kolibri/plugins/coach/frontend/views/attendance/__tests__/AttendancePages.spec.js +++ b/kolibri/plugins/coach/frontend/views/attendance/__tests__/AttendancePages.spec.js @@ -172,8 +172,9 @@ function renderEditPage({ } /** - * Helper to find a switch input by its name attribute. - * KSwitch renders as + * Gets an input element by its name attribute. + * @param {string} name - The name attribute of the input element. + * @returns {Element|null} The matching input element, or null if not found. */ function getSwitch(name) { return document.querySelector(`input[name="${name}"]`); diff --git a/kolibri/plugins/coach/frontend/views/common.js b/kolibri/plugins/coach/frontend/views/common.js index 1b069cad518..c6d8178056e 100644 --- a/kolibri/plugins/coach/frontend/views/common.js +++ b/kolibri/plugins/coach/frontend/views/common.js @@ -217,9 +217,10 @@ export default { } }, /** - * @param {Object[]} statuses - * @param {Date|null} statuses[].last_activity - * @return {Date|null} + * Find the most recent `last_activity` timestamp in a list of statuses. + * @param {object[]} statuses - Status objects to scan. + * @param {?Date} statuses[].last_activity - The activity timestamp for that status. + * @returns {?Date} The latest non-null timestamp, or null when none is available. */ maxLastActivity(statuses) { const max = this._.maxBy(statuses, 'last_activity'); diff --git a/kolibri/plugins/coach/frontend/views/common/assignments/AssignmentDetailsModal.vue b/kolibri/plugins/coach/frontend/views/common/assignments/AssignmentDetailsModal.vue index e5ee3f6f4bb..1e51d040454 100644 --- a/kolibri/plugins/coach/frontend/views/common/assignments/AssignmentDetailsModal.vue +++ b/kolibri/plugins/coach/frontend/views/common/assignments/AssignmentDetailsModal.vue @@ -217,9 +217,8 @@ }, props: { /** - * The assignment object to be edited - * @type {Object} - * @required + * The assignment object to be edited. + * @type {object} * @example * { * title: 'Assignment Title', @@ -410,6 +409,8 @@ } }, /** + * Re-enable the form and surface the generic server-error notice after a + * failed save. * @public */ handleSubmitFailure() { @@ -427,6 +428,8 @@ this.showServerError = true; }, /** + * Surface the title validation error and move focus back to the title + * field after a failed save. * @public */ handleSubmitTitleFailure() { @@ -437,6 +440,8 @@ window.scrollTo({ top: 0, behavior: 'smooth' }); }, /** + * Clear the displayed validation and server-error notices after a + * successful save. * @public */ handleSubmitSuccess() { @@ -445,6 +450,10 @@ this.showDeletedUsersError = false; }, /** + * Validate the title and recipients fields and return the first error. + * @param {boolean} [handleFailure] - When true (default), invoke the + * matching `handleSubmit*Failure` for any error encountered. + * @returns {string} The validation error message, or empty string when valid. * @public */ validate(handleFailure = true) { diff --git a/kolibri/plugins/coach/frontend/views/common/assignments/SidePanelRecipientsSelector/index.vue b/kolibri/plugins/coach/frontend/views/common/assignments/SidePanelRecipientsSelector/index.vue index cbc43eb024c..4af84421808 100644 --- a/kolibri/plugins/coach/frontend/views/common/assignments/SidePanelRecipientsSelector/index.vue +++ b/kolibri/plugins/coach/frontend/views/common/assignments/SidePanelRecipientsSelector/index.vue @@ -194,7 +194,8 @@ } }, /** - * Validates the selected recipients and sets the error message if invalid + * Validates the selected recipients and sets the error message if invalid. + * @returns {string} The validation error message, or empty string when valid. * @public */ validate() { @@ -206,6 +207,8 @@ return this.assignmentInvalidText; }, /** + * Announce the recipients validation error and refocus the radio button so + * a screen reader user is alerted after a failed submission. * @public */ async handleSubmitRecipientsFailure() { diff --git a/kolibri/plugins/coach/frontend/views/common/tables/ReportsLearnersTable.vue b/kolibri/plugins/coach/frontend/views/common/tables/ReportsLearnersTable.vue index 474a580d9b2..ca8d4ab5d34 100644 --- a/kolibri/plugins/coach/frontend/views/common/tables/ReportsLearnersTable.vue +++ b/kolibri/plugins/coach/frontend/views/common/tables/ReportsLearnersTable.vue @@ -196,6 +196,7 @@ return this.questionCount && !isUndefined(entry.statusObj.num_answered); }, /** + * Triggers a CSV download of the learner progress data currently displayed in the table. * @public */ exportCSV() { diff --git a/kolibri/plugins/coach/frontend/views/courses/composables/useAssignCourse.js b/kolibri/plugins/coach/frontend/views/courses/composables/useAssignCourse.js index 079bbf442c5..100d16c4395 100644 --- a/kolibri/plugins/coach/frontend/views/courses/composables/useAssignCourse.js +++ b/kolibri/plugins/coach/frontend/views/courses/composables/useAssignCourse.js @@ -12,10 +12,10 @@ import { useCourses } from '../../../composables/useCourses'; * * This is instantiated in the root component of the Assign Course side panel * and its state and methods will be passed down to child components through provide/inject. - * - * @param {Object} options **Required** Configuration options for the composable. - * @param {import('vue').Ref} options.classId **Required** The id of the class to which + * @param {object} options - **Required** Configuration options for the composable. + * @param {import('vue').Ref} options.classId - **Required** The id of the class to which * the course will be assigned. + * @returns {AssignCourseInjectObject} The assign-course composable API. */ export default function useAssignCourse({ classId }) { const searchKeywords = ref(''); @@ -54,7 +54,7 @@ export default function useAssignCourse({ classId }) { /** * Set existing course assignment data for editing - * @param {Object} courseSession - The course session object from CoursesRootPage + * @param {object} courseSession - The course session object from CoursesRootPage */ const setExistingAssignment = courseSession => { courseSessionId.value = courseSession.id; @@ -141,23 +141,27 @@ export default function useAssignCourse({ classId }) { /** * @typedef {import('kolibri-common/composables/useFetch.js').FetchObject} FetchObject - * - * @typedef {Object} AssignCourseInjectObject - * @property {import('vue').Ref} classId The id of the class to which the course will - * be assigned. - * @property {import('vue').Ref} searchKeywords The keywords used to search for courses. - * @property {FetchObject} coursesFetch The useFetch object for fetching courses. - * @property {import('vue').Ref} selectedCourse The currently selected course. - * @property {import('vue').Ref>} selectedGroupIds The ids of the selected groups - * to assign the course to. - * @property {import('vue').Ref>} selectedLearnerIds The ids of the selected learners - * to assign the course to. - * @property {(course: Object) => void} selectCourse Method to set the `selectedCourse` ref. - * @property {() => Promise} assignCourse Method to assign the selected course to the - * selected learners and groups. - * - * @returns {AssignCourseInjectObject} An object with properties and methods for managing - * the fetch process. + */ + +/** + * @typedef {object} AssignCourseInjectObject + * @property {import('vue').Ref} classId - The id of the class to which the course will + * be assigned. + * @property {import('vue').Ref} searchKeywords - The keywords used to search for courses. + * @property {FetchObject} coursesFetch - The useFetch object for fetching courses. + * @property {import('vue').Ref} selectedCourse - The currently selected course. + * @property {import('vue').Ref>} selectedGroupIds - The ids of the selected groups + * to assign the course to. + * @property {import('vue').Ref>} selectedLearnerIds - The ids of the selected + * learners to assign the course to. + * @property {(course: object) => void} selectCourse - Method to set the `selectedCourse` ref. + * @property {() => Promise} assignCourse - Method to assign the selected course to + * the selected learners and groups. + */ + +/** + * Inject the assign-course composable API provided by an ancestor `useAssignCourse` call. + * @returns {AssignCourseInjectObject} The injected composable API. */ export function injectAssignCourse() { return { diff --git a/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonLearnersTable.vue b/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonLearnersTable.vue index ced247eec40..5926b24a996 100644 --- a/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonLearnersTable.vue +++ b/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonLearnersTable.vue @@ -76,6 +76,7 @@ }, methods: { /** + * Triggers a CSV download of the learner progress data currently displayed in the table. * @public */ exportCSV() { diff --git a/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonResourcesTable.vue b/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonResourcesTable.vue index 2d40b3c86e4..afcfa6435c0 100644 --- a/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonResourcesTable.vue +++ b/kolibri/plugins/coach/frontend/views/lessons/LessonSummaryPage/tables/LessonResourcesTable.vue @@ -165,6 +165,7 @@ this.handleResourcesOrderChange({ newArray }); }, /** + * Triggers a CSV download of the resource progress data currently displayed in the table. * @public */ exportCSV() { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/TabsWithOverflow.vue b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/TabsWithOverflow.vue index 6a7fbb3e06f..23901465e5c 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/TabsWithOverflow.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/TabsWithOverflow.vue @@ -38,9 +38,9 @@ import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; /** - * @typedef {Object} Tab - * @property {string} label - * @property {string} id + * @typedef {object} Tab + * @property {string} label - User-facing label for the tab. + * @property {string} id - Unique identifier for the tab. */ export default { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue index 9f8dfad4d47..c82e3b0050d 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue @@ -189,6 +189,12 @@ import { SelectionTarget } from '../../../../common/resourceSelection/contants'; import autofocusFirstEl from '../../../../common/directives/autofocusFirstEl'; + /** + * @typedef {import('vue').Ref} Ref + * @typedef {import('../../../../../composables/quizCreationSpecs.js').QuizExercise} QuizExercise + * @typedef {import('../../../../../composables/quizCreationSpecs.js').QuizQuestion} QuizQuestions + */ + export default { name: 'QuizResourceSelection', components: { @@ -305,7 +311,8 @@ const { closeConfirmationTitle$, closeConfirmationMessage$ } = coachStrings; /** - * @param {QuizExercise[]} resources + * Merge exercises into the working resource pool, de-duplicating entries. + * @param {QuizExercise[]} resources - exercises to add to the pool * @affects workingResourcePool -- Updates it with the given resources and is ensured to have * a list of unique resources to avoid unnecessary duplication */ @@ -320,7 +327,8 @@ } /** - * @param {QuizExercise} content + * Remove matching exercises from the working resource pool. + * @param {QuizExercise[]} [resources] - exercises to remove, matched by id * @affects workingResourcePool - Remove given quiz exercise from workingResourcePool */ function removeFromWorkingResourcePool(resources = []) { @@ -330,6 +338,8 @@ } /** + * Replace the working resource pool with the given set of exercises. + * @param {QuizExercise[]} [resources] - exercises to set as the new pool * @affects workingResourcePool - Resets the workingResourcePool to the previous state */ function setWorkingResourcePool(resources = []) { @@ -337,7 +347,9 @@ } /** - * @param {QuizQuestions[]} questions + * Merge questions into the working questions list, de-duplicating entries. + * @param {QuizQuestions[]} questions - questions to add to the working list + * @param {QuizExercise} resource - exercise providing the questions, added to the pool if new * @affects workingQuestions -- Updates it with the given questions and is ensured to have * a list of unique questions to avoid unnecessary duplication */ @@ -349,7 +361,9 @@ } /** - * @param {QuizQuestions[]} questions + * Drop questions from the working list and prune any resources + * left with no selected questions. + * @param {QuizQuestions[]} questions - questions to remove, matched by `item` * @affects workingQuestions -- Removes the given questions from the workingQuestions */ function removeFromWorkingQuestions(questions) { @@ -399,6 +413,8 @@ /** * Practice quizzes should only be included if the resource selection side panel * is in `selectPracticeQuiz` mode. + * @param {QuizExercise} item - content node to test + * @returns {boolean} true when the item is eligible for the current mode */ const isPracticeQuiz = item => !selectPracticeQuiz || get(item, ['options', 'modality'], false) === Modalities.QUIZ; @@ -407,6 +423,8 @@ * Because survey questions have no 'correct' answer, and in some cases have a * 'correct answer'chosen at random for the sake of having one, * we should not allow them to be included in quizzes. + * @param {QuizExercise} item - content node to test + * @returns {boolean} true when the item is not a survey */ const isNotSurvey = item => get(item, ['options', 'modality'], false) !== Modalities.SURVEY; diff --git a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SearchQuizFilters.vue b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SearchQuizFilters.vue index 67784da9ec7..6be6994405f 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SearchQuizFilters.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SearchQuizFilters.vue @@ -22,6 +22,10 @@ import { useGoBack } from 'kolibri-common/composables/usePreviousRoute.js'; import { PageNames } from '../../../../../../constants'; + /** + * @typedef {import('kolibri-common/composables/useFetch.js').FetchObject} FetchObject + */ + export default { name: 'SearchQuizFilters', components: { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SelectFromQuizSearchResults.vue b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SelectFromQuizSearchResults.vue index 5eb5b7a9c7e..4e32de43e70 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SelectFromQuizSearchResults.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/SelectFromQuizSearchResults.vue @@ -64,6 +64,10 @@ import UpdatedResourceSelection from '../../../../../common/resourceSelection/UpdatedResourceSelection.vue'; import QuizResourceSelectionHeader from '../../../../../common/resourceSelection/QuizResourceSelectionHeader.vue'; + /** + * @typedef {import('kolibri-common/composables/useFetch.js').FetchObject} FetchObject + */ + export default { name: 'SelectFromQuizSearchResults', components: { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/index.vue b/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/index.vue index 5817507fe6e..4e2f1d2fa61 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/index.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/index.vue @@ -250,6 +250,8 @@ }, methods: { /** + * Called by the router guard after page data is fetched successfully to populate the view. + * @param {object} data - Page data returned by the API, including difficult questions. * @public */ setData(data) { @@ -259,6 +261,8 @@ pageLoading.value = false; }, /** + * Called by the router guard when page data fetching fails to surface the error. + * @param {object} error - The error returned by the failed API request. * @public */ setError(error) { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/tables/DifficultQuestionsTable.vue b/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/tables/DifficultQuestionsTable.vue index e938e322b23..16627ca9de1 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/tables/DifficultQuestionsTable.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/QuizSummaryPage/tables/DifficultQuestionsTable.vue @@ -95,6 +95,7 @@ }); }, /** + * Triggers a CSV download of the difficult questions data currently displayed in the table. * @public */ exportCSV() { diff --git a/kolibri/plugins/coach/frontend/views/quizzes/reports/QuizPreviewPage.vue b/kolibri/plugins/coach/frontend/views/quizzes/reports/QuizPreviewPage.vue index 24777021e51..edb200889cf 100644 --- a/kolibri/plugins/coach/frontend/views/quizzes/reports/QuizPreviewPage.vue +++ b/kolibri/plugins/coach/frontend/views/quizzes/reports/QuizPreviewPage.vue @@ -95,6 +95,8 @@ }, methods: { /** + * Called by the router guard after page data is fetched successfully to populate the view. + * @param {object} data - Page data returned by the API, including exam and exercise nodes. * @public */ setData(data) { @@ -105,6 +107,8 @@ pageLoading.value = false; }, /** + * Called by the router guard when page data fetching fails to surface the error. + * @param {object} error - The error returned by the failed API request. * @public */ setError(error) { diff --git a/kolibri/plugins/device/frontend/modules/deviceInfo/handlers.js b/kolibri/plugins/device/frontend/modules/deviceInfo/handlers.js index 60aef052bde..6ea62aef7fc 100644 --- a/kolibri/plugins/device/frontend/modules/deviceInfo/handlers.js +++ b/kolibri/plugins/device/frontend/modules/deviceInfo/handlers.js @@ -37,10 +37,10 @@ export function getDeviceInfo() { } /** - * Action to hydrate device-info page. - * - * @param {Store} store - * @returns Promise + * Fetches device info and commits it to the Vuex store for the device info page. + * @param {object} store - The Vuex store instance. + * @param {object} route - The current Vue Router route object. + * @returns {Promise} Resolves when device info has been loaded into the store. */ export function showDeviceInfoPage(store, route) { const { canManageContent } = useUser(); diff --git a/kolibri/plugins/device/frontend/modules/manageContent/utils.js b/kolibri/plugins/device/frontend/modules/manageContent/utils.js index a796262229e..b098f42dd07 100644 --- a/kolibri/plugins/device/frontend/modules/manageContent/utils.js +++ b/kolibri/plugins/device/frontend/modules/manageContent/utils.js @@ -3,10 +3,11 @@ import { TaskStatuses } from 'kolibri-common/utils/syncTaskUtils'; /** * Watches the state.taskList and resolves when the tracked Task is COMPLETED. - * - * @param {string} taskId - * @returns {Promise} - * + * @param {string} taskId - The id of the task to track. + * @param {object} [store] - The Vuex store to watch (defaults to the core store). + * @returns {Promise<{taskId: string, cancelled: boolean}>} Resolves when the task + * completes (or is cancelled, in which case `cancelled` is true). Rejects when the + * task transitions to a failed state. */ export function waitForTaskToComplete(taskId, store = coreStore) { const taskList = state => state.manageContent.taskList; diff --git a/kolibri/plugins/device/frontend/modules/userPermissions/actions.js b/kolibri/plugins/device/frontend/modules/userPermissions/actions.js index dd35c3355d5..9f438de8466 100644 --- a/kolibri/plugins/device/frontend/modules/userPermissions/actions.js +++ b/kolibri/plugins/device/frontend/modules/userPermissions/actions.js @@ -3,11 +3,10 @@ import FacilityUserResource from 'kolibri-common/apiResources/FacilityUserResour import { handleApiError } from 'kolibri/utils/appError'; /** - * Adds or modifies a DevicePermissions model. - * - * @param {boolean} payload.is_superuser - * @param {boolean} payload.can_manage_content - * @returns Promise + * Saves or updates device permissions for a user and commits the updated state to the store. + * @param {object} store - The Vuex store instance. + * @param {object} payload - Permissions payload with userId, is_superuser, and can_manage_content. + * @returns {Promise} Resolves with the updated user model. */ export function addOrUpdateUserPermissions(store, payload) { const permissions = { diff --git a/kolibri/plugins/device/frontend/modules/userPermissions/handlers.js b/kolibri/plugins/device/frontend/modules/userPermissions/handlers.js index 673d3078137..a016034add4 100644 --- a/kolibri/plugins/device/frontend/modules/userPermissions/handlers.js +++ b/kolibri/plugins/device/frontend/modules/userPermissions/handlers.js @@ -8,11 +8,11 @@ import { handleApiError } from 'kolibri/utils/appError'; import { pageLoading } from 'kolibri-common/composables/usePageLoading'; /** - * Serially fetches Permissions, then FacilityUser. If returned Promise rejects, - * it is from the request for FacilityUser. - * - * @param {string} userId - * @returns Promise<{ permissions, user }, FacilityUserError> + * Serially fetches Permissions, then FacilityUser. If the returned Promise rejects, it + * is from the request for FacilityUser. + * @param {string} userId - The ID of the user to fetch permissions for. + * @returns {Promise<{permissions: object, user: object}>} Resolves with combined + * permissions and user data. */ function fetchUserPermissions(userId) { const permissionsPromise = DevicePermissionsResource.fetchModel({ id: userId, force: true }); @@ -39,11 +39,11 @@ function fetchUserPermissions(userId) { } /** - * Action to hydrate user-permissions-page. - * - * @param {Store} store - * @param {string} userId - * @returns Promise + * Load the user permissions page state into the Vuex store. + * @param {object} store - The Vuex store instance. + * @param {string} userId - The ID of the user whose permissions to display. + * @param {object} route - The current Vue Router route object. + * @returns {Promise} Resolves when state has been loaded. */ export function showUserPermissionsPage(store, userId, route) { const { fetchFacilities } = useFacilities(); diff --git a/kolibri/plugins/device/frontend/modules/wizard/actions/availableChannelsActions.js b/kolibri/plugins/device/frontend/modules/wizard/actions/availableChannelsActions.js index 12a0cdf4e05..686fe1dd5ee 100644 --- a/kolibri/plugins/device/frontend/modules/wizard/actions/availableChannelsActions.js +++ b/kolibri/plugins/device/frontend/modules/wizard/actions/availableChannelsActions.js @@ -2,14 +2,19 @@ import differenceBy from 'lodash/differenceBy'; import find from 'lodash/find'; import { getRemoteChannelByToken } from '../utils'; +/** + * @typedef {import('kolibri-common/apiResources/RemoteChannelResource').Channel} Channel + */ + /** * HACK: Makes a request to Kolibri Studio to get info on unlisted channels, then appends - * them to the public channels. This hack is to get around the fact that the ChannelMetadata object - * does not indicate the origins of a channel: whether a remote public, remote unlisted, or bespoke - * channel from USB, the ChannelMetadata is identical. - * - * @param {Array} publicChannels - the list of publich channels, which will not be queried - * @returns {Promise>} + * them to the public channels. This hack is to get around the fact that the ChannelMetadata + * object does not indicate the origins of a channel: whether a remote public, remote unlisted, + * or bespoke channel from USB, the ChannelMetadata is identical. + * @param {object} store - The Vuex store instance. + * @param {Array} publicChannels - The list of public channels, which will not be + * queried. + * @returns {Promise>} Resolves with the combined channel list. */ export function getAllRemoteChannels(store, publicChannels) { const { channelList } = store.rootState.manageContent; diff --git a/kolibri/plugins/device/frontend/modules/wizard/actions/contentTreeViewerActions.js b/kolibri/plugins/device/frontend/modules/wizard/actions/contentTreeViewerActions.js index ab095e4d279..62fbaa7add3 100644 --- a/kolibri/plugins/device/frontend/modules/wizard/actions/contentTreeViewerActions.js +++ b/kolibri/plugins/device/frontend/modules/wizard/actions/contentTreeViewerActions.js @@ -12,11 +12,9 @@ function isDescendantOrSelf(testNode, selfNode) { } /** - * Queries the server for the current total file size and resource count - * and then sets it to the store. - * - * @returns {Promise} - * + * Fetches and commits the file size and resource count for the current transfer selection. + * @param {object} store - The Vuex store instance with transfer state. + * @returns {Promise} Resolves when the transfer size has been updated in the store. */ function setImportExportFileSizeAndResourceCount(store) { const { transferredChannel, nodesForTransfer } = store.state; @@ -59,11 +57,10 @@ function setImportExportFileSizeAndResourceCount(store) { } /** - * Adds a new node to the transfer list. - * - * @param node {Node} - Node to be added - * @param node.path {Array} - path (via ids) from channel root to the Node - * + * Adds a content node to the transfer include list and updates the transfer size. + * @param {object} store - The Vuex store instance with transfer state. + * @param {object} node - The content node to add to the transfer. + * @returns {Promise} Resolves when the node has been added and transfer size updated. */ export function addNodeForTransfer(store, node) { const { included, omitted } = store.state.nodesForTransfer; @@ -84,10 +81,10 @@ export function addNodeForTransfer(store, node) { } /** - * Removes node from transfer list - * - * @param node {Node} - node to be removed - * + * Removes a content node from the transfer include list and updates the transfer size. + * @param {object} store - The Vuex store instance with transfer state. + * @param {object} node - The content node to remove from the transfer. + * @returns {Promise} Resolves when the node has been removed and transfer size updated. */ export function removeNodeForTransfer(store, node) { const forImport = !store.getters.inExportMode; diff --git a/kolibri/plugins/device/frontend/modules/wizard/actions/selectContentActions.js b/kolibri/plugins/device/frontend/modules/wizard/actions/selectContentActions.js index 5e0535a673f..e88427d3df8 100644 --- a/kolibri/plugins/device/frontend/modules/wizard/actions/selectContentActions.js +++ b/kolibri/plugins/device/frontend/modules/wizard/actions/selectContentActions.js @@ -4,8 +4,9 @@ import { downloadChannelMetadata } from '../utils'; import { getChannelWithContentSizes } from '../apiChannelMetadata'; /** - * Transitions the import/export wizard to the 'load-channel-metadata' interstitial state - * + * Loads channel metadata for the current transfer, downloading it if needed. + * @param {object} store - The Vuex store instance with wizard state. + * @returns {Promise} Resolves when channel metadata has been loaded into the store. */ export function loadChannelMetadata(store) { let dbPromise; @@ -48,13 +49,9 @@ export function loadChannelMetadata(store) { } /** - * Makes a call to freespace API and places result in the store. - * If transfer type is LOCALEXPORT, it gets the selected drive's freespace. - * - * @param {string} path - Path to the Kolibri data folder. - * If empty, defaults to server's KOLIBRI_HOME. - * @returns {Promise} - * + * Gets the available free space on a drive, or on the device's content storage if no drive given. + * @param {object} selectedDrive - Optional drive object with a freespace property. + * @returns {Promise} Resolves with the available free space in bytes, or -1 on error. */ export function getAvailableSpaceOnDrive(selectedDrive) { if (selectedDrive) { diff --git a/kolibri/plugins/device/frontend/modules/wizard/handlers.js b/kolibri/plugins/device/frontend/modules/wizard/handlers.js index 085968ab3e3..c15309d1aed 100644 --- a/kolibri/plugins/device/frontend/modules/wizard/handlers.js +++ b/kolibri/plugins/device/frontend/modules/wizard/handlers.js @@ -143,8 +143,10 @@ export function showAvailableChannelsPage(store, params, route) { } /** - * Handler for going to Select Content Page URL directly - * params are { channel_id: string, drive_id?: string, address_id? }, + * Loads the select content page state for a given channel and transfer type. + * @param {object} store - The Vuex store instance. + * @param {object} params - Route params with drive_id, address_id, and channel_id. + * @returns {Promise} Resolves when the select content page state is ready. */ export function showSelectContentPage(store, params) { let selectedDrivePromise = Promise.resolve({}); @@ -255,10 +257,10 @@ export function showSelectContentPage(store, params) { } /** - * Updates wizardState.treeView when a new topic is clicked. - * - * @param {Object} topic - { id, title, path } - * + * Fetches granular content node data for a topic and updates the wizard tree view. + * @param {object} store - The Vuex store instance with wizard state. + * @param {object} topic - The topic content node object with id and title. + * @returns {Promise} Resolves when the tree view topic has been updated in the store. */ export function updateTreeViewTopic(store, topic) { const fetchArgs = {}; diff --git a/kolibri/plugins/device/frontend/modules/wizard/utils.js b/kolibri/plugins/device/frontend/modules/wizard/utils.js index 801ee29b1ec..34c5f5fbffa 100644 --- a/kolibri/plugins/device/frontend/modules/wizard/utils.js +++ b/kolibri/plugins/device/frontend/modules/wizard/utils.js @@ -8,11 +8,10 @@ import { waitForTaskToComplete } from '../manageContent/utils'; import { getChannelWithContentSizes } from './apiChannelMetadata'; /** - * Makes request to RemoteChannel API with a token. Does not actually interact - * with Vuex store. - * - * @param {string} token - - * @returns Promise + * Makes a request to the RemoteChannel API with a token. Does not actually interact with + * the Vuex store. + * @param {string} token - The channel token to look up. + * @returns {Promise} Resolves with the remote channel object. */ export function getRemoteChannelByToken(token) { return RemoteChannelResource.fetchModel({ id: token, force: true }); @@ -23,10 +22,13 @@ export function getRemoteChannelBundleByToken(token) { } /** - * Starts Task that downloads a Channel Metadata database. - * NOTE: cannot be normally dispatched as an action, since it uses - * waitForTaskToComplete (which relies on the store singleton with a .watch method) + * Starts a task that downloads a Channel Metadata database. * + * NOTE: cannot be normally dispatched as an action, since it uses `waitForTaskToComplete` + * (which relies on the store singleton with a `.watch` method). + * @param {object} store - The Vuex store instance; defaults to the core store singleton. + * @returns {Promise} Resolves with the channel content sizes when the task completes. + * @throws {Error} If the wizard is not in an import mode when called. */ export function downloadChannelMetadata(store = coreStore) { if ( diff --git a/kolibri/plugins/device/frontend/views/ManageContentPage/ManageChannelContentsPage/index.vue b/kolibri/plugins/device/frontend/views/ManageContentPage/ManageChannelContentsPage/index.vue index 8b21217b27b..dbb466bdef1 100644 --- a/kolibri/plugins/device/frontend/views/ManageContentPage/ManageChannelContentsPage/index.vue +++ b/kolibri/plugins/device/frontend/views/ManageContentPage/ManageChannelContentsPage/index.vue @@ -316,8 +316,8 @@ this.shownModal = null; }, /** + * Refreshes the page data after a watched content task completes, handling channel deletion. * @public - * Used by the taskNotificationMixin to handle the completion of the task */ onWatchedTaskFinished() { // clear out the nodeCache diff --git a/kolibri/plugins/device/frontend/views/ManageContentPage/index.vue b/kolibri/plugins/device/frontend/views/ManageContentPage/index.vue index 97b3862a11a..e8bf71531ce 100644 --- a/kolibri/plugins/device/frontend/views/ManageContentPage/index.vue +++ b/kolibri/plugins/device/frontend/views/ManageContentPage/index.vue @@ -264,8 +264,8 @@ TaskResource.clearAll(); }, /** + * Refreshes the channel list after a watched content task completes. * @public - * Used by the taskNotificationMixin to handle the completion of the task */ onWatchedTaskFinished() { this.refreshChannelList(); diff --git a/kolibri/plugins/device/frontend/views/PinAuthenticationModal.vue b/kolibri/plugins/device/frontend/views/PinAuthenticationModal.vue index a49286bca84..28e44549d6c 100644 --- a/kolibri/plugins/device/frontend/views/PinAuthenticationModal.vue +++ b/kolibri/plugins/device/frontend/views/PinAuthenticationModal.vue @@ -64,9 +64,9 @@ } /** - * Validate a PIN code against a facility dataset - * @param {string} pinCode - The 4-digit PIN to validate - * @returns {Promise} - True if PIN is valid, false otherwise + * Validate a PIN code against a facility dataset. + * @param {string} pinCode - The 4-digit PIN to validate. + * @returns {Promise} - True if PIN is valid, false otherwise. */ async function validatePin(pinCode) { const response = await client({ @@ -78,8 +78,8 @@ } /** - * Submit pin for validation and handle response - * @return {Promise} + * Submit pin for validation and handle response. + * @returns {Promise} */ async function submit() { if (!pin.value) { diff --git a/kolibri/plugins/device/frontend/views/SelectContentPage/index.vue b/kolibri/plugins/device/frontend/views/SelectContentPage/index.vue index dbc55ddd990..7792ccc0f74 100644 --- a/kolibri/plugins/device/frontend/views/SelectContentPage/index.vue +++ b/kolibri/plugins/device/frontend/views/SelectContentPage/index.vue @@ -318,8 +318,8 @@ }); }, /** + * Called by the parent after the watched import or export task has finished. * @public - * Used by the taskNotificationMixin to handle the completion of the task */ onWatchedTaskFinished() { // After import task has finished, refresh so those nodes will be disabled diff --git a/kolibri/plugins/device/frontend/views/SelectContentPage/treeViewUtils.js b/kolibri/plugins/device/frontend/views/SelectContentPage/treeViewUtils.js index 6dd22cfe2b1..95b365dffb6 100644 --- a/kolibri/plugins/device/frontend/views/SelectContentPage/treeViewUtils.js +++ b/kolibri/plugins/device/frontend/views/SelectContentPage/treeViewUtils.js @@ -4,6 +4,13 @@ import sumBy from 'lodash/fp/sumBy'; import { createTranslator } from 'kolibri/utils/i18n'; import { selectContentTopicLink } from '../ManageContentPage/manageContentLinks'; +/** + * @typedef {{included: object[], omitted: object[]}} SelectedNodes + */ +/** + * @typedef {object & {message: string, disabled: boolean, checkboxType: string}} AnnotatedNode + */ + const translator = createTranslator('TreeViewRowMessages', { alreadyOnYourDevice: { message: 'Already on your device', @@ -98,15 +105,13 @@ function unselectedNode(node) { } /** - * Takes a Node, plus contextual data from store, then annotates them with info - * needed to correctly display it on tree view. - * - * @param node {Node} - * @param selectedNodes {SelectedNodes} - * @param selectedNodes.omitted {Array} - * @param selectedNodes.included {Array} - * @returns {AnnotatedNode} - annotations are message, disabled, and checkboxType - * + * Takes a Node, plus contextual data from store, then annotates it with the info needed to + * correctly display it on tree view. + * @param {Node} node - The content node to annotate. + * @param {SelectedNodes} selectedNodes - Selection state containing `included` and `omitted` + * arrays of nodes. + * @param {boolean} forImport - Whether the tree is being used for import (vs export/manage). + * @returns {AnnotatedNode} Annotated node; annotations are message, disabled, and checkboxType. */ export function annotateNode(node, selectedNodes, forImport = true) { const { on_device_resources, total_resources } = node; @@ -202,9 +207,12 @@ export function annotateNode(node, selectedNodes, forImport = true) { } /** - * Takes an array of breadcrumb { id, title } objects in state, and converts them - * into a form that can be used in k-breadcrumbs props.items { text, link: LinkObject }. - * + * Transforms a content node into a breadcrumb link object for the tree view. + * @param {object} node - The content node to transform. + * @param {object} route - Route context object. + * @param {object} route.query - The current route query parameters. + * @param {object} route.params - The current route params including channel_id. + * @returns {object} Breadcrumb object with text and link properties. */ export function transformBreadrumb(node, { query, params }) { return { diff --git a/kolibri/plugins/device/frontend/views/commonDeviceStrings.js b/kolibri/plugins/device/frontend/views/commonDeviceStrings.js index 87846d85c38..8d5bdc8ec79 100644 --- a/kolibri/plugins/device/frontend/views/commonDeviceStrings.js +++ b/kolibri/plugins/device/frontend/views/commonDeviceStrings.js @@ -70,9 +70,10 @@ export const deviceStrings = createTranslator('CommonDeviceStrings', { }); /** - * @param {string} key Key mapped to a string above - * @param {args} Args that would be passed to a string deviced with ICU message syntax - * @returns {string} + * Returns a translated device string for the given key and optional arguments. + * @param {string} key - The translation key from CommonDeviceStrings. + * @param {object} args - Optional interpolation arguments for the translation. + * @returns {string} The translated string. */ export function deviceString(key, args) { return deviceStrings.$tr(key, args); diff --git a/kolibri/plugins/device/frontend/views/lodUsers/composables/useLodDeviceUsers.js b/kolibri/plugins/device/frontend/views/lodUsers/composables/useLodDeviceUsers.js index 8a81d55febf..42696e49f7e 100644 --- a/kolibri/plugins/device/frontend/views/lodUsers/composables/useLodDeviceUsers.js +++ b/kolibri/plugins/device/frontend/views/lodUsers/composables/useLodDeviceUsers.js @@ -28,7 +28,6 @@ const SOUD_QUEUE = 'soud_sync'; * - Fetching and managing users from the LOD device. * - Handling the import of users with different methods (as admin, with credentials). * - Managing the state and route redirection of the import process using an XState machine. - * * @returns {void} */ export default function useLodDeviceUsers() { @@ -134,8 +133,8 @@ export default function useLodDeviceUsers() { } /** - * Filter running importLODUser tasks whose users are not already in the usersBeingImported - * and add them to the usersBeingImported list. + * Adds any running import tasks not yet tracked by the machine to the import list. + * @returns {void} */ function _addMissingUsersToImportList() { tasks.value @@ -228,15 +227,14 @@ export default function useLodDeviceUsers() { /** * Injects the state and methods from the LOD device users composable. - * - * @typedef {Object} InjectLodDeviceUsersObject + * @typedef {object} InjectLodDeviceUsersObject * @property {Array} users The list of users fetched from the LOD device. * @property {boolean} loading The loading state of the users. - * @property {Object} remoteAdmin The remote admin user information + * @property {object} remoteAdmin The remote admin user information * that is set in the "import as admin" xstate machine flow. * @property {Array} remoteUsers The list of remote users fetched from the remote device. * @property {string|null} importDeviceId The ID of the device from which users are being imported. - * @property {Object} selectedFacility The facility selected to import users from. + * @property {object} selectedFacility The facility selected to import users from. * @property {Array} usersBeingImported The list of users currently being imported. * @property {boolean} showCannotRemoveUser Flag to indicate if the "cannot remove user" * message should be shown. @@ -246,7 +244,6 @@ export default function useLodDeviceUsers() { * @property {Function} removeUser Function to remove a user from the LOD device. * @property {Function} resetShowCannotRemoveUser Function to reset the * "cannot remove user" message. - * * @returns {InjectLodDeviceUsersObject} An object containing the state and methods. */ export function injectLodDeviceUsers() { diff --git a/kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue b/kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue index be992042138..1a1fd85f8d5 100644 --- a/kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue +++ b/kolibri/plugins/epub_viewer/frontend/views/SearchSideBar.vue @@ -96,9 +96,9 @@ * Searches through an entire book for a string, but caps after a certain amount of results is * exceeded * Helpful in preventing a CPU lockup for search queries with a lot of search results - * @param {object} book Ebookjs book object - * @param {string} searchQuery String to search for - * @param {number} maxSearchResults Stop searching for matches after this is amount is exceeded, + * @param {object} book - Epubjs book object + * @param {string} searchQuery - String to search for + * @param {number} maxSearchResults - Stop searching for matches after this is amount is exceeded, * e.g. 1000 * @returns {Promise} A promise that resolves to the search results */ @@ -174,6 +174,7 @@ }, methods: { /** + * Move keyboard focus to the search input so the user can start typing. * @public */ focusOnInput() { @@ -201,8 +202,8 @@ /** * This method "marks" the match text to which the cfi refers in every result in the * search results list - * @param {array} searchResults - * @returns {array} searchResults with the excerpt split into before, match, and after + * @param {Array} searchResults - Raw search results returned by the epub search + * @returns {Array} searchResults with the excerpt split into before, match, and after * where the match is the text that will be highlighted */ selectMatchResult(searchResults) { @@ -232,10 +233,10 @@ * Identify the index of the match in the result excerpt based * on the distance between the cfis with the next result compared to * the distance of the result excerpt between a match and the next match. - * @param {object} params - * @param {string[]} params.textSplit The result excerpt split by the search query - * @param {string} params.cfi The cfi of the current result - * @param {object} params.nextResult The next result + * @param {object} params - Arguments bundled as a single object + * @param {string[]} params.textSplit - The result excerpt split by the search query + * @param {string} params.cfi - The cfi of the current result + * @param {object} params.nextResult - The result that immediately follows the current one * @returns {number} The index of the match in the result excerpt * @example If there are n matches in the same result excerpt, this method will * return 0 if the cfi refers to the first match, 1 for the second match, etc. @@ -266,8 +267,8 @@ * If two matches appears in the same result excerpt then they are in the same node. * So, this method finds the distance between the two matches and return -1 if they are * not in the same node. - * @param {string} cfi1 The cfi of the first match - * @param {string} cfi2 The cfi of the second match after the first one + * @param {string} cfi1 - The cfi of the first match + * @param {string} cfi2 - The cfi of the second match after the first one * @returns {number} The number of characters between the two matches or -1 if they are * not in the same node. */ @@ -301,10 +302,10 @@ /** * Divide the excerpt into before, match, and after based on the index of the match * where the match is the text that will be highlighted - * @param {object} params - * @param {string[]} params.textSplit The result excerpt split by the search query - * @param {string} params.excerpt The result excerpt - * @param {number} params.selectedIndex The index of the match in the result excerpt + * @param {object} params - Arguments bundled as a single object + * @param {string[]} params.textSplit - The result excerpt split by the search query + * @param {string} params.excerpt - The result excerpt + * @param {number} params.selectedIndex - The index of the match in the result excerpt * @returns {object} The excerpt divided into before, match, and after */ splitExcerpt({ textSplit, excerpt, selectedIndex }) { diff --git a/kolibri/plugins/epub_viewer/frontend/views/TopBar.vue b/kolibri/plugins/epub_viewer/frontend/views/TopBar.vue index e2abee13561..e066a27518c 100644 --- a/kolibri/plugins/epub_viewer/frontend/views/TopBar.vue +++ b/kolibri/plugins/epub_viewer/frontend/views/TopBar.vue @@ -84,18 +84,21 @@ }, methods: { /** + * Moves keyboard focus to the table of contents button. * @public */ focusOnTocButton() { this.$refs.tocButton.$el.focus(); }, /** + * Moves keyboard focus to the settings button. * @public */ focusOnSettingsButton() { this.$refs.settingsButton.$el.focus(); }, /** + * Moves keyboard focus to the search button. * @public */ focusOnSearchButton() { diff --git a/kolibri/plugins/facility/frontend/composables/useActionWithUndo.js b/kolibri/plugins/facility/frontend/composables/useActionWithUndo.js index 8fc9a372454..5392ec694fa 100644 --- a/kolibri/plugins/facility/frontend/composables/useActionWithUndo.js +++ b/kolibri/plugins/facility/frontend/composables/useActionWithUndo.js @@ -2,31 +2,30 @@ import { bulkUserManagementStrings } from 'kolibri-common/strings/bulkUserManage import useSnackbar from 'kolibri/composables/useSnackbar'; /** - * - * @param {Object} options + * @typedef {object} UseActionWithUndoObject + * @property {() => Promise} performAction - A method to manually trigger the main action + * with all the undo machinery set up. + */ + +/** + * Wraps an action with an undo snackbar, providing a `performAction` function that runs the + * action and shows a snackbar with an undo option when it succeeds. + * @param {object} options - Options object. * @param {() => Promise} options.action - Callback that executes the action to perform. - * Should return a boolean promise indicating whether the action was successful. If the action - * succeeds, the snackbar will display a success message and provide an undo option. - * + * Should return a boolean promise indicating whether the action was successful. If the action + * succeeds, the snackbar will display a success message and provide an undo option. * @param {() => string} options.actionNotice$ - Function that returns the message to display on - * the snackbar when the action is successful. - * - * @param {() => Promise} options.undoAction - Callback that executes the undo action. - * Be careful if this action is happening after the component that triggered the original action has - * been unmounted (e.g. you cannot emit events from an unmounted component). If the undoAction fails - * the function should throw an error, and a snackbar will be shown with a generic error message. - * - * @param {() => string} options.undoActionNotice$ - Function that returns the message to display on - * the snackbar when the undo action is successful. - * + * the snackbar when the action is successful. + * @param {() => Promise} options.undoAction - Callback that executes the undo action. Be + * careful if this action is happening after the component that triggered the original action + * has been unmounted (e.g. you cannot emit events from an unmounted component). If the + * undoAction fails the function should throw an error, and a snackbar will be shown with a + * generic error message. + * @param {() => string} options.undoActionNotice$ - Function that returns the message to display + * on the snackbar when the undo action is successful. * @param {() => void} [options.onBlur] - Optional callback that executes when the undo button in - * the snackbar loses focus. - * - * @typedef {Object} UseActionWithUndoObject - * @property {() => Promise} performAction - A method to manually trigger the main action - * with all the undo machinery set up. - * - * @returns {UseActionWithUndoObject} + * the snackbar loses focus. + * @returns {UseActionWithUndoObject} Undo-wrapped action helpers. */ export default function useActionWithUndo({ action, diff --git a/kolibri/plugins/facility/frontend/composables/useFacilityEditor.js b/kolibri/plugins/facility/frontend/composables/useFacilityEditor.js index 8c297ea84f0..6e6d6838a8f 100644 --- a/kolibri/plugins/facility/frontend/composables/useFacilityEditor.js +++ b/kolibri/plugins/facility/frontend/composables/useFacilityEditor.js @@ -10,7 +10,9 @@ import { OptionsForSignIn, PicturePasswordIconStyle } from 'kolibri-common/const import { useFacilityConfig } from 'kolibri-common/composables/useFacility'; /** - * @param {string} facilityId The ID of the facility to edit + * Composable providing facility editor state and actions for the facility settings page. + * @param {string} facilityId - The ID of the facility to edit. + * @returns {object} Facility editor state, computed properties, and action methods. */ export default function useFacilityEditor(facilityId) { const { fetchFacilities, getFacility } = useFacilities(); @@ -79,7 +81,8 @@ export default function useFacilityEditor(facilityId) { } /** - * Loads the facility and it's config into the composable state + * Fetches the facility configuration and name from the backend. + * @returns {Promise} Resolves when facility data has been loaded. */ async function fetchFacility() { setLoading(true); @@ -158,9 +161,9 @@ export default function useFacilityEditor(facilityId) { } /** - * Updates the facility's name - * @param {string} name - * @return {Promise<*>} + * Saves a new facility name to the backend and refreshes the facilities list. + * @param {string} name - The new facility name to save. + * @returns {Promise} Resolves with the updated facility model. */ async function saveFacilityName(name) { const facility = await FacilityResource.saveModel({ diff --git a/kolibri/plugins/facility/frontend/composables/usePagination.js b/kolibri/plugins/facility/frontend/composables/usePagination.js index 268e600ad65..527b891d2aa 100644 --- a/kolibri/plugins/facility/frontend/composables/usePagination.js +++ b/kolibri/plugins/facility/frontend/composables/usePagination.js @@ -4,8 +4,11 @@ import pickBy from 'lodash/pickBy'; import clamp from 'lodash/clamp'; /** - * Composable for managing pagination state and navigation - * Handles page changes, items per page, and URL query synchronization + * Composable providing pagination state and navigation for the users table. + * @param {object} options - Options object. + * @param {object} options.usersCount - A ref or computed with the total number of users. + * @param {object} options.totalPages - A ref or computed with the total number of pages. + * @returns {object} Pagination state and navigation methods. */ export default function usePagination({ usersCount, totalPages } = {}) { const route = useRoute(); diff --git a/kolibri/plugins/facility/frontend/composables/useUsersFilters.js b/kolibri/plugins/facility/frontend/composables/useUsersFilters.js index 4d7d0a5f4e8..1c72a2c1b45 100644 --- a/kolibri/plugins/facility/frontend/composables/useUsersFilters.js +++ b/kolibri/plugins/facility/frontend/composables/useUsersFilters.js @@ -10,10 +10,21 @@ import { DateRangeFilters } from '../constants'; /** * Composable to manage user filters in the user management pages. - * - * @param {object} options - * @param {Array} options.classes - Ref to the list of classes available for filtering. - * @returns + * @param {object} options - Composable options. + * @param {import('vue').Ref>} options.classes - Ref to the + * list of classes available for filtering. + * @returns {{ + * routeFilters: import('vue').ComputedRef, + * workingFilters: object, + * numAppliedFilters: import('vue').ComputedRef, + * classesOptions: import('vue').ComputedRef>, + * userFilterOptions: Array<{id: string, label: string, icon: string}>, + * creationDateOptions: Array<{value: string, label: string, dateSubtraction?: object}>, + * applyFilters: (options?: {nextRouteName?: string}) => void, + * resetFilters: () => void, + * getBackendFilters: () => object, + * resetWorkingFilters: () => void, + * }} The user-filter state, options, and methods. */ export default function useUsersFilters({ classes }) { const router = useRouter(); @@ -134,10 +145,9 @@ export default function useUsersFilters({ classes }) { * Apply the current filters to the route by updating the query parameters, * and pushing the new route. This will remove from the query any filters * that are not longer applied, but will leave any other query parameters intact. - * - * @param {object} options - * @param {string} options.nextRouteName - The name of the route to navigate to - * after applying filters. + * @param {object} [options] - Routing overrides applied alongside the new query. + * @param {string} [options.nextRouteName] - The name of the route to navigate to + * after applying filters; defaults to the current route name. */ const applyFilters = ({ nextRouteName } = {}) => { const nextQuery = { ...route.query }; diff --git a/kolibri/plugins/facility/frontend/composables/useUsersTableSearch.js b/kolibri/plugins/facility/frontend/composables/useUsersTableSearch.js index 5183cb2f1aa..d1d78f5d085 100644 --- a/kolibri/plugins/facility/frontend/composables/useUsersTableSearch.js +++ b/kolibri/plugins/facility/frontend/composables/useUsersTableSearch.js @@ -4,8 +4,8 @@ import pickBy from 'lodash/pickBy'; import debounce from 'lodash/debounce'; /** - * Composable for managing search functionality in the Users table - * Handles search term state and URL query parameter synchronization + * Composable providing search state and methods for the facility users table. + * @returns {object} Search term ref, textbox ref, and methods to focus and clear the search. */ export default function useUsersTableSearch() { const route = useRoute(); diff --git a/kolibri/plugins/facility/frontend/modules/classEditManagement/actions.js b/kolibri/plugins/facility/frontend/modules/classEditManagement/actions.js index 52f299d0f3c..94f5fa3bf3e 100644 --- a/kolibri/plugins/facility/frontend/modules/classEditManagement/actions.js +++ b/kolibri/plugins/facility/frontend/modules/classEditManagement/actions.js @@ -46,9 +46,12 @@ export function removeClassCoach(store, { classId, userId }) { } /** - * Do a PATCH to update the class. - * @param {string} id - class id. - * @param {object} updateData. + * Updates a class with the given data and commits the change to the store. + * @param {object} store - The Vuex store instance. + * @param {object} payload - Payload object. + * @param {string} payload.id - The ID of the class to update. + * @param {object} payload.updateData - The data to update on the class. + * @returns {Promise|void} Resolves when the class has been updated. */ export function updateClass(store, { id, updateData }) { if (!id || Object.keys(updateData).length === 0) { diff --git a/kolibri/plugins/facility/frontend/modules/classManagement/actions.js b/kolibri/plugins/facility/frontend/modules/classManagement/actions.js index d2e0ccc8baa..179d62f366e 100644 --- a/kolibri/plugins/facility/frontend/modules/classManagement/actions.js +++ b/kolibri/plugins/facility/frontend/modules/classManagement/actions.js @@ -2,8 +2,10 @@ import ClassroomResource from 'kolibri-common/apiResources/ClassroomResource'; import { handleApiError } from 'kolibri/utils/appError'; /** - * Do a POST to create new class - * @param {string} name + * Creates a new class with the given name and adds it to the store. + * @param {object} store - The Vuex store instance. + * @param {string} name - The name for the new class. + * @returns {Promise} Resolves when the class has been created. */ export function createClass(store, name) { return ClassroomResource.saveModel({ diff --git a/kolibri/plugins/facility/frontend/modules/userManagement/actions.js b/kolibri/plugins/facility/frontend/modules/userManagement/actions.js index 10b38929469..11af4f12714 100644 --- a/kolibri/plugins/facility/frontend/modules/userManagement/actions.js +++ b/kolibri/plugins/facility/frontend/modules/userManagement/actions.js @@ -4,12 +4,11 @@ import { UserKinds } from 'kolibri/constants'; import { updateFacilityLevelRoles } from './utils'; /** - * Does a POST request to assign a user role (only used in this file) - * @param {Object} user - * @param {string} user.id - * @param {string} user.facility - * @param {string} user.roles - * Needed: id, facility, role + * Does a POST request to assign a user role (only used in this file). Needed fields on `user`: + * `id`, `facility`. + * @param {object} user - The facility user object. + * @param {object} role - The role object; `role.kind` is the role kind to assign. + * @returns {Promise} Resolves with the refreshed user model. */ function setUserRole(user, role) { return updateFacilityLevelRoles(user, role.kind).then(() => { @@ -19,9 +18,11 @@ function setUserRole(user, role) { } /** - * Do a POST to create new user - * @param {object} stateUserData - * Needed: username, full_name, facility, role, password + * Does a POST to create a new facility user. Needed fields on `payload`: `username`, `full_name`, + * `facility`, `role`, `password`. + * @param {object} store - The Vuex store instance. + * @param {object} payload - User creation data: username, password, role, and demographics. + * @returns {Promise} Resolves when the user has been created. */ export function createFacilityUser(store, payload) { return FacilityUserResource.saveModel({ diff --git a/kolibri/plugins/facility/frontend/modules/userManagement/utils.js b/kolibri/plugins/facility/frontend/modules/userManagement/utils.js index 2c9507abe46..308713e0a71 100644 --- a/kolibri/plugins/facility/frontend/modules/userManagement/utils.js +++ b/kolibri/plugins/facility/frontend/modules/userManagement/utils.js @@ -5,12 +5,15 @@ import RoleResource from 'kolibri-common/apiResources/RoleResource'; const FACILITY_ROLES = [UserKinds.ADMIN, UserKinds.ASSIGNABLE_COACH, UserKinds.COACH]; /** - * Implements business logic for changing a FacilityUser's Role - * - * If Learner/New User -> ASSIGNABLE_COACH/COACH/ADMIN, then create that Role - * If ASSIGNABLE_COACH/COACH/ADMIN -> LEARNER, then delete all Classroom-Level Coach Roles - * IF ASSIGNABLE_COACH/COACH/ADMIN -> ASSIGNABLE_COACH/COACH/ADMIN, then replace only that Role + * Implements business logic for changing a FacilityUser's Role. * + * If Learner/New User -> ASSIGNABLE_COACH/COACH/ADMIN, then create that Role. + * If ASSIGNABLE_COACH/COACH/ADMIN -> LEARNER, then delete all Classroom-Level Coach Roles. + * IF ASSIGNABLE_COACH/COACH/ADMIN -> ASSIGNABLE_COACH/COACH/ADMIN, then replace only that Role. + * @param {object} facilityUser - The user whose facility-level role is being updated. + * @param {string} newRoleKind - The desired UserKinds value for the user's facility role. + * @returns {Promise|undefined} Promise resolving once the role updates have + * been persisted, or undefined when no transition is supported by this helper. */ export function updateFacilityLevelRoles(facilityUser, newRoleKind) { const { roles, facility, id } = facilityUser; diff --git a/kolibri/plugins/learn/frontend/apiResources.js b/kolibri/plugins/learn/frontend/apiResources.js index 44749f4fce3..784d68e6fa8 100644 --- a/kolibri/plugins/learn/frontend/apiResources.js +++ b/kolibri/plugins/learn/frontend/apiResources.js @@ -1,8 +1,7 @@ import { Resource } from 'kolibri/apiResource'; /** - * Gets all of the Classrooms in which a Learner is enrolled - * + * Gets all of the Classrooms in which a Learner is enrolled. * @example To get Classrooms without assignments and progress: * LearnerClassroomResource.fetchCollection({ * getParams: { no_assignments: true }, diff --git a/kolibri/plugins/learn/frontend/composables/__mocks__/useDevices.js b/kolibri/plugins/learn/frontend/composables/__mocks__/useDevices.js index f9077aa7f62..c8928931abf 100644 --- a/kolibri/plugins/learn/frontend/composables/__mocks__/useDevices.js +++ b/kolibri/plugins/learn/frontend/composables/__mocks__/useDevices.js @@ -8,7 +8,7 @@ * If you need to override some default values from some tests, * you can import a helper function `useDevicesMock` that accepts * an object with values to be overriden and use it together - * with `mockImplementation` as follows: + * with `mockImplementation`, as shown in the example below. * * ``` * // eslint-disable-next-line import-x/named diff --git a/kolibri/plugins/learn/frontend/composables/__mocks__/useDownloadRequests.js b/kolibri/plugins/learn/frontend/composables/__mocks__/useDownloadRequests.js index 322845bad19..aa7763b2628 100644 --- a/kolibri/plugins/learn/frontend/composables/__mocks__/useDownloadRequests.js +++ b/kolibri/plugins/learn/frontend/composables/__mocks__/useDownloadRequests.js @@ -8,7 +8,7 @@ * If you need to override some default values from some tests, * you can import a helper function `useDownloadRequestsMock` that accepts * an object with values to be overriden and use it together - * with `mockImplementation` as follows: + * with `mockImplementation`, as shown in the example below. * * ``` * // eslint-disable-next-line import-x/named diff --git a/kolibri/plugins/learn/frontend/composables/__mocks__/useLearnerResources.js b/kolibri/plugins/learn/frontend/composables/__mocks__/useLearnerResources.js index 63418495671..d8de5f93d5b 100644 --- a/kolibri/plugins/learn/frontend/composables/__mocks__/useLearnerResources.js +++ b/kolibri/plugins/learn/frontend/composables/__mocks__/useLearnerResources.js @@ -8,7 +8,7 @@ * If you need to override some default values from some tests, * you can import a helper function `useLearnerResourcesMock` that accepts * an object with values to be overriden and use it together - * with `mockImplementation` as follows: + * with `mockImplementation`, as shown in the example below. * * ``` * // eslint-disable-next-line import-x/named diff --git a/kolibri/plugins/learn/frontend/composables/__mocks__/usePinnedDevices.js b/kolibri/plugins/learn/frontend/composables/__mocks__/usePinnedDevices.js index 35dd8a8b3b3..a8e76996147 100644 --- a/kolibri/plugins/learn/frontend/composables/__mocks__/usePinnedDevices.js +++ b/kolibri/plugins/learn/frontend/composables/__mocks__/usePinnedDevices.js @@ -8,7 +8,7 @@ * If you need to override some default values from some tests, * you can import a helper function `usePinnedDevicesMock` that accepts * an object with values to be overriden and use it together - * with `mockImplementation` as follows: + * with `mockImplementation`, as shown in the example below. * * ``` * // eslint-disable-next-line import-x/named diff --git a/kolibri/plugins/learn/frontend/composables/useBookmarks.js b/kolibri/plugins/learn/frontend/composables/useBookmarks.js index 1b25fb60c31..639020b7b3a 100644 --- a/kolibri/plugins/learn/frontend/composables/useBookmarks.js +++ b/kolibri/plugins/learn/frontend/composables/useBookmarks.js @@ -23,8 +23,7 @@ const loadingBookmarksMap = reactive({}); /** * Adds or updates a bookmark in the shared bookmarksMap. - * - * @param {Object} bookmark - A bookmark object with at least `contentnode_id` and `id`. + * @param {object} bookmark - A bookmark object with at least `contentnode_id` and `id`. */ export function setBookmark(bookmark) { set(bookmarksMap, bookmark.contentnode_id, bookmark); @@ -32,7 +31,6 @@ export function setBookmark(bookmark) { /** * Removes a bookmark from the shared bookmarksMap by contentnode_id. - * * @param {string} contentnodeId - The content node ID to remove from the map. */ export function clearBookmark(contentnodeId) { @@ -42,7 +40,6 @@ export function clearBookmark(contentnodeId) { /** * Creates a bookmark on the server and updates the local bookmarksMap. * On failure, removes the entry from the map. - * * @param {string} contentnodeId - The content node ID to bookmark. * @returns {Promise} Promise that resolves with the created bookmark data. */ @@ -71,7 +68,6 @@ export async function createBookmark(contentnodeId) { /** * Removes a bookmark from the server and updates the local bookmarksMap. * On failure, restores the bookmark back into the map. - * * @param {string} contentnodeId - The content node ID whose bookmark to remove. * @returns {Promise} Promise that resolves when the bookmark is deleted. */ @@ -99,8 +95,7 @@ export async function removeBookmark(contentnodeId) { export default function useBookmarks() { /** * Fetches bookmarks data and saves data to this composable's store. - * - * @param {Object} getParams - Parameters to filter by (e.g. { contentnode_id }). + * @param {object} getParams - Parameters to filter by (e.g. { contentnode_id }). * @returns {Promise} Promise that resolves with the fetched bookmarks array. * @public */ diff --git a/kolibri/plugins/learn/frontend/composables/useContentLink.js b/kolibri/plugins/learn/frontend/composables/useContentLink.js index 3c39b31394f..df960749ce6 100644 --- a/kolibri/plugins/learn/frontend/composables/useContentLink.js +++ b/kolibri/plugins/learn/frontend/composables/useContentLink.js @@ -39,10 +39,12 @@ export default function useContentLink() { * A function to generate a VueRouter link object that links to * either a resource or a topic, and generates query parameters * that allow creating a backlink to the route context in which - * this link is generated - * @param {string} id - the id of the node - * @param {boolean} isResource - whether this is a resource or not - * @return {Object} VueRouter link object + * this link is generated. + * @param {string} id - The id of the node. + * @param {boolean} isResource - Whether this is a resource or not. + * @param {string} [deviceId] - Override for the device id route parameter; defaults + * to the device id on the current route. + * @returns {object} VueRouter link object. */ function genContentLinkBackLinkCurrentPage(id, isResource = false, deviceId) { const query = _getBackLinkQuery(); @@ -69,9 +71,10 @@ export default function useContentLink() { * while maintaining the conceptual model of a single immersive overlay * that can be closed out, returning to the originating page that linked * to the original resource. - * @param {string} id - the id of the node - * @param {boolean} isResource - whether this is a resource or not - * @return {Object} VueRouter link object + * @param {string} id - The id of the node. + * @param {boolean} isResource - Whether this is a resource or not. + * @param {string} [deviceId] - Override for the device id route parameter. + * @returns {object} VueRouter link object. */ function genContentLinkKeepCurrentBackLink(id, isResource = false, deviceId) { const oldQuery = route.query; @@ -88,8 +91,9 @@ export default function useContentLink() { * while maintaining the conceptual model of a single immersive overlay * that can be closed out, returning to the originating page that linked * to the original parent topic of the resource. - * @param {string} id - the id of the node - * @return {Object} VueRouter link object + * @param {string} id - The id of the node. + * @param {string} [deviceId] - Override for the device id route parameter. + * @returns {object} VueRouter link object. */ function genContentLinkKeepPreviousBackLink(id, deviceId) { const oldQuery = _decodeBackLinkQuery(route.query); diff --git a/kolibri/plugins/learn/frontend/composables/useContentNodeProgress.js b/kolibri/plugins/learn/frontend/composables/useContentNodeProgress.js index 8e3c9c939e1..614aaad55eb 100644 --- a/kolibri/plugins/learn/frontend/composables/useContentNodeProgress.js +++ b/kolibri/plugins/learn/frontend/composables/useContentNodeProgress.js @@ -32,11 +32,11 @@ export function setContentNodeProgress(progress) { export default function useContentNodeProgress() { /** * Fetches content node progress data - * and saves data to this composable's store - * - * @param {Object} getParams Parameters to filter by, should be the same as + * and saves data to this composable's store. + * @param {object} getParams - Parameters to filter by, should be the same as * the contentnodes fetched that we want the progress for. - * @returns {Promise} + * @returns {Promise} Resolves once the progress data has been merged into + * the shared store. * @public */ function fetchContentNodeProgress(getParams) { @@ -52,12 +52,15 @@ export default function useContentNodeProgress() { } /** - * Fetches content node progress data - * and saves data to this composable's store - * - * @param {Object} getParams Parameters to filter by, should be the same as + * Fetches content node tree progress data and saves data to this composable's store. + * @param {object} getParams - Parameters to filter by, should be the same as * the contentnodes fetched that we want the progress for. - * @returns {Promise} + * @param {string} getParams.id - The id of the parent content node whose tree progress + * should be fetched. + * @param {object} getParams.params - GET parameters forwarded to the tree progress + * endpoint (e.g. pagination state). + * @returns {Promise} Resolves once the progress data has been merged into + * the shared store. * @public */ function fetchContentNodeTreeProgress({ id, params }) { diff --git a/kolibri/plugins/learn/frontend/composables/useCoreLearn.js b/kolibri/plugins/learn/frontend/composables/useCoreLearn.js index 56b2440abf7..48290d9b9a4 100644 --- a/kolibri/plugins/learn/frontend/composables/useCoreLearn.js +++ b/kolibri/plugins/learn/frontend/composables/useCoreLearn.js @@ -4,6 +4,8 @@ import client from 'kolibri/client'; import urls from 'kolibri/urls'; import plugin_data from 'kolibri-plugin-data'; +/** @typedef {import('vue').Ref} Ref */ + /** * Whether the user is in any classes * @type {Ref} @@ -30,9 +32,11 @@ export function prepareLearnApp() { } /** - * @return {{ + * Returns the learn-app's reactive state flags for classroom membership and download + * permissions. + * @returns {{ * canDownloadExternally: Ref, canAddDownloads: Ref, inClasses: Ref - * }} + * }} Reactive learn-app state. */ export default function useCoreLearn() { return { diff --git a/kolibri/plugins/learn/frontend/composables/useDevices.js b/kolibri/plugins/learn/frontend/composables/useDevices.js index 65ba1b194cb..56ac3c61b38 100644 --- a/kolibri/plugins/learn/frontend/composables/useDevices.js +++ b/kolibri/plugins/learn/frontend/composables/useDevices.js @@ -16,8 +16,8 @@ import { KolibriStudioId } from '../constants'; import { learnStrings } from '../views/commonLearnStrings'; /** - * The ref is defined in the outer scope so it can be used as a shared store - * @type {Ref} + * The ref is defined in the outer scope so it can be used as a shared store. + * @type {import('vue').Ref} */ const currentDevice = ref(null); @@ -78,9 +78,12 @@ export function setCurrentDevice(id) { } /** - * @param {string|null} routingDeviceId - * @param {function(NetworkLocation):*} callback - * @return {ComputedRef<*|null>} + * Build a computed ref that runs `callback` against the current device only when + * the device's `instance_id` matches the routed device id. + * @param {import('vue').Ref} routingDeviceId - Reactive device id from the route. + * @param {(device: object) => unknown} callback - Selector applied to the matching device. + * @returns {import('vue').ComputedRef} Computed ref returning the selector's + * result, or undefined when the current device is not the routed device. */ function computedDevice(routingDeviceId, callback) { return computed(() => { diff --git a/kolibri/plugins/learn/frontend/composables/useLearnerResources.js b/kolibri/plugins/learn/frontend/composables/useLearnerResources.js index 3e06a7efc7c..0126e43b868 100644 --- a/kolibri/plugins/learn/frontend/composables/useLearnerResources.js +++ b/kolibri/plugins/learn/frontend/composables/useLearnerResources.js @@ -152,7 +152,7 @@ export default function useLearnerResources() { }); /** - * @returns {Boolean} - `true` if a learner finished all active + * @returns {boolean} - `true` if a learner finished all active * classes lessons and quizzes (or when there are none) * @public */ @@ -167,8 +167,9 @@ export default function useLearnerResources() { }); /** - * @param {String} classId - * @returns {Object} A class + * Look up a cached learner classroom by id. + * @param {string} classId - Classroom id to find + * @returns {object} A class * @public */ function getClass(classId) { @@ -176,7 +177,8 @@ export default function useLearnerResources() { } /** - * @param {String} classId + * Return the active lessons on the classroom with the given id. + * @param {string} classId - Classroom id * @returns {Array} All active lessons of a class * @public */ @@ -189,7 +191,8 @@ export default function useLearnerResources() { } /** - * @param {String} classId + * Return the active quizzes on the classroom with the given id. + * @param {string} classId - Classroom id * @returns {Array} All active quizzes of a class * @public */ @@ -202,7 +205,8 @@ export default function useLearnerResources() { } /** - * @param {String} classId + * Return the courses assigned to the classroom with the given id. + * @param {string} classId - Classroom id * @returns {Array} All courses of a class * @public */ @@ -215,8 +219,9 @@ export default function useLearnerResources() { } /** - * @param {Object} lesson - * @returns {Object} vue-router link to a lesson page + * Build a router target for opening a classroom lesson playlist. + * @param {object} lesson - Lesson record from the classroom payload + * @returns {object} vue-router link to a lesson page * @public */ function getClassLessonLink(lesson) { @@ -233,8 +238,10 @@ export default function useLearnerResources() { } /** - * @param {Object} quiz - * @returns {Object} vue-router link to a quiz report page when the quiz + * Build a router target for a classroom quiz, routing to the report view + * when the quiz is closed and to the viewer otherwise. + * @param {object} quiz - Exam record from the classroom payload + * @returns {object} vue-router link to a quiz report page when the quiz * is closed. Otherwise returns a link to a quiz page. * @public */ @@ -265,8 +272,9 @@ export default function useLearnerResources() { } /** - * @param {Object} course - * @returns {Object} vue-router link to a course page (placeholder) + * Build a router target for a classroom course page. + * @param {object} course - Course record from the classroom payload + * @returns {object} vue-router link to a course page (placeholder) * @public */ function getClassCourseLink(course) { @@ -284,10 +292,10 @@ export default function useLearnerResources() { /** * Fetches a class by its ID and saves data * to this composable's store - * - * @param {String} classId - * @param {Boolean} force Cache won't be used when `true` - * @returns {Promise} + * @param {object} params - Request parameters + * @param {string} params.classId - Classroom id to load + * @param {boolean} [params.force] - Cache won't be used when `true` + * @returns {Promise} - Resolves with the loaded classroom * @public */ function fetchClass({ classId, force = false }) { @@ -302,9 +310,9 @@ export default function useLearnerResources() { /** * Fetches current learner's classes * and saves data to this composable's store - * - * @param {Boolean} force Cache won't be used when `true` - * @returns {Promise} + * @param {object} [params] - Request parameters + * @param {boolean} [params.force] - Cache won't be used when `true` + * @returns {Promise} - Resolves once the classes store has been populated * @public */ function fetchClasses({ force = false } = {}) { @@ -323,8 +331,7 @@ export default function useLearnerResources() { /** * Fetches resumable content nodes with their progress data * and saves data to this composable's store - * - * @returns {Promise} + * @returns {Promise} - Resolves with the resumable nodes array * @public */ function fetchResumableContentNodes() { @@ -347,8 +354,7 @@ export default function useLearnerResources() { /** * Fetches more resumable content nodes with their progress data * and saves data to this composable's store - * - * @returns {Promise} + * @returns {Promise} - Resolves with the next page of resumable nodes * @public */ function fetchMoreResumableContentNodes() { @@ -373,8 +379,9 @@ export default function useLearnerResources() { }); /** - * @param {String} courseId - * @returns {Object} Course content tree + * Return the cached content tree for the given course. + * @param {string} courseId - Course identifier + * @returns {object} Course content tree * @public */ function getCourseContent(courseId) { @@ -382,8 +389,9 @@ export default function useLearnerResources() { } /** - * @param {String} courseId - * @returns {Object} Course progress data + * Return the cached progress data for the given course. + * @param {string} courseId - Course identifier + * @returns {object} Course progress data * @public */ function getCourseProgress(courseId) { @@ -391,8 +399,9 @@ export default function useLearnerResources() { } /** - * @param {String} courseId - * @returns {Array} Course units + * Return the top-level unit nodes of the given course. + * @param {string} courseId - Course identifier whose units to read + * @returns {Array} Ordered unit nodes from the cached course tree * @public */ function getCourseUnits(courseId) { @@ -402,10 +411,10 @@ export default function useLearnerResources() { /** * Fetches a course by its session ID and saves data * to this composable's store - * - * @param {String} courseSessionId - * @param {Boolean} force Cache won't be used when `true` - * @returns {Promise} Course data + * @param {object} params - Request parameters + * @param {string} params.courseSessionId - Learner course session id + * @param {boolean} [params.force] - Cache won't be used when `true` + * @returns {Promise} Course data * @public */ async function fetchCourse({ courseSessionId, force = false }) { @@ -436,9 +445,9 @@ export default function useLearnerResources() { /** * Fetches current learner's courses * and saves data to this composable's store - * - * @param {Boolean} force Cache won't be used when `true` - * @returns {Promise} + * @param {object} [params] - Request parameters + * @param {boolean} [params.force] - Cache won't be used when `true` + * @returns {Promise} - Resolves with the loaded course collection * @public */ async function fetchCourses({ force = false } = {}) { @@ -448,8 +457,11 @@ export default function useLearnerResources() { } /** - * @param {String} testType - 'pre' or 'post' - * @returns {Boolean} Whether the test is currently available + * Report whether the pre- or post-test for a unit is currently offered. + * @param {string} courseId - Course identifier + * @param {string} unitId - Unit identifier + * @param {string} testType - 'pre' or 'post' + * @returns {boolean} Whether the test is currently available * @public */ function isUnitTestAvailable(courseId, unitId, testType) { @@ -464,10 +476,11 @@ export default function useLearnerResources() { } /** - * @param {String} courseId - * @param {String} unitId - * @param {String} lessonId - * @returns {Boolean} Whether the lesson resource is available + * Report whether the learner has progressed far enough to open this lesson. + * @param {string} courseId - Course identifier + * @param {string} unitId - Unit identifier containing the lesson + * @param {string} lessonId - Lesson identifier to check + * @returns {boolean} Whether the lesson resource is available * @public */ function isCourseLessonAvailable(courseId, unitId, lessonId) { @@ -523,10 +536,11 @@ export default function useLearnerResources() { } /** - * @param {String} courseId - * @param {String} unitId - * @param {String} lessonId - * @returns {Boolean} Whether this is the current lesson being worked on + * Report whether the given lesson is the learner's current resume position. + * @param {string} courseId - Course identifier + * @param {string} unitId - Unit identifier containing the lesson + * @param {string} lessonId - Lesson identifier to test + * @returns {boolean} Whether this is the current lesson being worked on * @public */ function isCurrentCourseLesson(courseId, unitId, lessonId) { diff --git a/kolibri/plugins/learn/frontend/composables/useProgressTracking.js b/kolibri/plugins/learn/frontend/composables/useProgressTracking.js index 99014c9d79c..341496ca970 100644 --- a/kolibri/plugins/learn/frontend/composables/useProgressTracking.js +++ b/kolibri/plugins/learn/frontend/composables/useProgressTracking.js @@ -315,6 +315,15 @@ export default function useProgressTracking() { /** * Initialize a content session for progress tracking * To be called on page load for content viewers + * @param {object} [params] - Session parameters + * @param {object} [params.node] - ContentNode being tracked + * @param {string} [params.lessonId] - Owning lesson id + * @param {string} [params.quizId] - Quiz id when tracking a quiz session + * @param {boolean} [params.repeat] - Force a new session even if one exists + * @param {string} [params.courseSessionId] - Identifier for a course session + * @param {string} [params.courseTest] - Identifier for a course test session + * @returns {Promise|undefined} - Resolves with session payload; undefined if resumed + * @throws {TypeError} - When no strategy-identifying parameter is provided */ function initContentSession({ node, @@ -491,6 +500,16 @@ export default function useProgressTracking() { /** * Update a content session for progress tracking + * @param {object} [params] - Update parameters + * @param {number} [params.progressDelta] - Incremental progress to add + * @param {number} [params.progress] - Absolute progress value in [0, 1] + * @param {object} [params.contentState] - Serialisable content-state payload + * @param {object} [params.interaction] - Attempt/interaction record + * @param {boolean} [params.immediate] - Flush immediately rather than debouncing + * @param {boolean} [params.force] - Send update even if nothing would otherwise change + * @returns {Promise} - Resolves once the debounced update round-trips + * @throws {ReferenceError} - When no session has been initialised + * @throws {TypeError} - When arguments are invalid or mutually exclusive */ function updateContentSession({ progressDelta, @@ -607,7 +626,6 @@ export default function useProgressTracking() { /** * Start interval timer and set start time - * @param {int} interval */ function startTrackingProgress() { timeCheckIntervalTimer = setInterval(updateContentSession, intervalTime); @@ -617,6 +635,9 @@ export default function useProgressTracking() { /** * Stop interval timer and update latest times * Must be called after startTrackingProgress + * @returns {Promise|undefined} - Resolves with final session payload; + * undefined when no session is active + * @throws {Error} - Re-throws unexpected errors not caused by a missing session */ function stopTrackingProgress() { clearTrackingInterval(); diff --git a/kolibri/plugins/learn/frontend/modules/pluginModule.js b/kolibri/plugins/learn/frontend/modules/pluginModule.js index 3f057e1936a..028fd8bc92a 100644 --- a/kolibri/plugins/learn/frontend/modules/pluginModule.js +++ b/kolibri/plugins/learn/frontend/modules/pluginModule.js @@ -19,7 +19,7 @@ export default { /** * Used as a Learn-global state to allow communication about whether this modal is shown * or not at any time. It should be set as `false` whenever the content page is loaded. - **/ + */ showCompleteContentModal: false, }; }, diff --git a/kolibri/plugins/learn/frontend/views/AlsoInThis.vue b/kolibri/plugins/learn/frontend/views/AlsoInThis.vue index 53e525daf09..115e4d7ab11 100644 --- a/kolibri/plugins/learn/frontend/views/AlsoInThis.vue +++ b/kolibri/plugins/learn/frontend/views/AlsoInThis.vue @@ -220,7 +220,10 @@ }, }, computed: { - /** Overrides some default styles in KRouterLink */ + /** + * Overrides some default styles in KRouterLink. + * @returns {object} The CSS style overrides applied to KRouterLink. + */ linkStyles() { return { color: this.$themeTokens.text + '!important', diff --git a/kolibri/plugins/learn/frontend/views/BrowseResourceMetadata.vue b/kolibri/plugins/learn/frontend/views/BrowseResourceMetadata.vue index 45ca63f6770..736eae50e3d 100644 --- a/kolibri/plugins/learn/frontend/views/BrowseResourceMetadata.vue +++ b/kolibri/plugins/learn/frontend/views/BrowseResourceMetadata.vue @@ -288,15 +288,15 @@ computed: { /** * Returns whether or not the LearnerNeeds.FOR_BEGINNERS constant is present in - * this.content.learner_needs - * @returns {Boolean} + * this.content.learner_needs. + * @returns {boolean} True if the content is flagged for beginners. */ forBeginners() { return get(this.content, 'learner_needs', []).includes(LearnerNeeds.FOR_BEGINNERS); }, /** - * Returns a list of this.content.learner_needs without the FOR_BEGINNERS key, if present - * @returns {string[]} + * Returns a list of this.content.learner_needs without the FOR_BEGINNERS key, if present. + * @returns {string[]} Learner needs with FOR_BEGINNERS filtered out. */ learnerNeeds() { // Remove FOR_BEGINNERS in this list because it is indicated separately, above, if present @@ -305,15 +305,15 @@ ); }, /** - * Joins this.content.accessibility_labels with a comma & space for display purposes - * @returns {string} + * Joins this.content.accessibility_labels with a comma & space for display purposes. + * @returns {string} The comma-separated accessibility labels. */ accessibilityLabels() { return this.content.accessibility_labels.map(label => this.coreString(label)).join(', '); }, /** - * Joins this.learnerNeeds with a comma & space for display purposes - * @returns {string} + * Joins this.learnerNeeds with a comma & space for display purposes. + * @returns {string} The comma-separated learner needs labels. */ learnerNeedsLabels() { return this.learnerNeeds.map(label => this.coreString(label)).join(', '); @@ -407,8 +407,8 @@ } }, /** + * Determines and calls first focusable element for FocusTrap. * @public - * Determines and calls first focusable element for FocusTrap */ focusFirstEl() { this.$refs.resourceButton.$el.focus(); diff --git a/kolibri/plugins/learn/frontend/views/CompletionModal/CompletionModalSection.vue b/kolibri/plugins/learn/frontend/views/CompletionModal/CompletionModalSection.vue index 0f9b751f55f..31144fb27d0 100644 --- a/kolibri/plugins/learn/frontend/views/CompletionModal/CompletionModalSection.vue +++ b/kolibri/plugins/learn/frontend/views/CompletionModal/CompletionModalSection.vue @@ -124,7 +124,7 @@ default: '', }, /** - * vue-router link object + * Vue-router link object * The action button is rendered as a link targeting * this route if provided, otherwise it's rendered as * a button that emits `buttonClick` event. @@ -178,6 +178,8 @@ }, methods: { /** + * Returns a reference to the primary action button element. + * @returns {object} The button element ref. * @public */ getButtonRef() { diff --git a/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/ResourceLabel.vue b/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/ResourceLabel.vue index cdaa685abdd..93636834a75 100644 --- a/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/ResourceLabel.vue +++ b/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/ResourceLabel.vue @@ -58,7 +58,7 @@ required: true, }, /** - * vue-router link object + * Vue-router link object * If provided, a resource title will be rendered * as a link targeting this route. */ diff --git a/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/index.vue b/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/index.vue index 914da10456d..3d7c0ec6dc0 100644 --- a/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/index.vue +++ b/kolibri/plugins/learn/frontend/views/CompletionModal/ResourceItem/index.vue @@ -72,7 +72,7 @@ required: true, }, /** - * vue-router link object + * Vue-router link object * The whole component behaves like * a link when provided. */ diff --git a/kolibri/plugins/learn/frontend/views/CompletionModal/index.vue b/kolibri/plugins/learn/frontend/views/CompletionModal/index.vue index 8860790849a..fb1216b6031 100644 --- a/kolibri/plugins/learn/frontend/views/CompletionModal/index.vue +++ b/kolibri/plugins/learn/frontend/views/CompletionModal/index.vue @@ -413,9 +413,9 @@ this.$router.push(this.nextContentNodeRoute); }, /** + * Handles focus events to trap focus within the modal when appropriate. + * @param {Event} event - The focus event to evaluate. * @public - * Focuses on correct first element for FocusTrap depending on content - * rendered in CompletionModal. */ focusElementTest(event) { const { target } = event; diff --git a/kolibri/plugins/learn/frontend/views/CourseUnitView/CourseContentViewer.vue b/kolibri/plugins/learn/frontend/views/CourseUnitView/CourseContentViewer.vue index 02196de763b..196174648ab 100644 --- a/kolibri/plugins/learn/frontend/views/CourseUnitView/CourseContentViewer.vue +++ b/kolibri/plugins/learn/frontend/views/CourseUnitView/CourseContentViewer.vue @@ -250,7 +250,7 @@ }, /** * Previous resource in the course unit. Used mainly as navigation fallback in case there - * isn't any other way to get out of the current viewer (e.g. no next resource available,but + * isn't any other way to get out of the current viewer (e.g., no next resource available, but * no other way to get out of the current resource except going back to the previous one). */ previousResource: { diff --git a/kolibri/plugins/learn/frontend/views/CourseUnitView/UnitTreeAccordion/index.vue b/kolibri/plugins/learn/frontend/views/CourseUnitView/UnitTreeAccordion/index.vue index 51ec098eb12..44d0622e6d1 100644 --- a/kolibri/plugins/learn/frontend/views/CourseUnitView/UnitTreeAccordion/index.vue +++ b/kolibri/plugins/learn/frontend/views/CourseUnitView/UnitTreeAccordion/index.vue @@ -344,7 +344,7 @@ default: null, }, /** - * Whether the current unit is already completed. i.e. we are seeing a + * Whether the current unit is already completed. I.e., we are seeing a * unit previous to the current unit in the unit tree. */ isUnitComplete: { diff --git a/kolibri/plugins/learn/frontend/views/CourseUnitView/__tests__/CourseUnitView.spec.js b/kolibri/plugins/learn/frontend/views/CourseUnitView/__tests__/CourseUnitView.spec.js index b3a411e7cef..44ed7db38cc 100644 --- a/kolibri/plugins/learn/frontend/views/CourseUnitView/__tests__/CourseUnitView.spec.js +++ b/kolibri/plugins/learn/frontend/views/CourseUnitView/__tests__/CourseUnitView.spec.js @@ -122,6 +122,11 @@ describe('CourseUnitView', () => { * Sets up a full unit tree mock with lessons and resources so that * the `shouldRedirectToResumePosition` checks can find the resource * in the tree structure. + * @param {object} [config] - Tree configuration. + * @param {string} [config.unitId] - id of the unit at the root of the mocked tree. + * @param {Array} [config.lessonIds] - Lesson ids to include as children of the unit. + * @param {{[lessonId: string]: Array}} [config.resourceIdsByLesson] - Map of + * lesson id to the resource ids placed under that lesson. */ function setupUnitTree({ unitId = UNIT_1, diff --git a/kolibri/plugins/learn/frontend/views/CourseUnitView/index.vue b/kolibri/plugins/learn/frontend/views/CourseUnitView/index.vue index e106a246245..8572fd558c5 100644 --- a/kolibri/plugins/learn/frontend/views/CourseUnitView/index.vue +++ b/kolibri/plugins/learn/frontend/views/CourseUnitView/index.vue @@ -416,8 +416,11 @@ }; /** - * Redirect to a valid position if the current unit is previous to the resume position unit - * or if resume position doesn't have where to resume within the unit + * Redirect to a valid position if the current unit is previous to the resume + * position unit, or if resume position doesn't have where to resume within + * the unit. + * @returns {boolean} True if a redirect was issued, false otherwise. + * @throws {Error} If no resource can be found to redirect to when one is required. */ const checkRedirectToUnitTree = () => { if ( @@ -524,6 +527,7 @@ /** * If we need to redirect to resume_position, it is because the current route * is invalid or is currently on the resume position. + * @returns {boolean} True if the route should be redirected to the resume position. */ const shouldRedirectToResumePosition = () => { if (!props.unitId) { diff --git a/kolibri/plugins/learn/frontend/views/CourseUnitView/useCourseContentProgressTracking.js b/kolibri/plugins/learn/frontend/views/CourseUnitView/useCourseContentProgressTracking.js index 405993e9745..554565caa16 100644 --- a/kolibri/plugins/learn/frontend/views/CourseUnitView/useCourseContentProgressTracking.js +++ b/kolibri/plugins/learn/frontend/views/CourseUnitView/useCourseContentProgressTracking.js @@ -27,13 +27,12 @@ const MasteryCriterionKey = Symbol('MasteryCriterion'); * Composable that manages progress tracking for course content. * Should be instantiated in CourseUnitView and provides progress state * to child components via Vue's provide/inject mechanism. - * - * @param {Object} options **Required** Configuration options for the composable. - * @param {import('vue').Ref} options.contentNode **optional** Reactive ref to the + * @param {object} options - **Required** Configuration options for the composable. + * @param {import('vue').Ref} options.contentNode - **optional** Reactive ref to the * current content node. - * @param {import('vue').Ref} options.courseSessionId **Required** Reactive ref to the + * @param {import('vue').Ref} options.courseSessionId - **Required** Reactive ref to the * course session ID. - * @param {import('vue').Ref} options.activeTest **Optional** Reactive ref to the + * @param {import('vue').Ref} options.activeTest - **Optional** Reactive ref to the * active test information, if applicable. */ export default function useCourseContentProgress({ contentNode, courseSessionId, activeTest }) { @@ -71,7 +70,11 @@ export default function useCourseContentProgress({ contentNode, courseSessionId, }; /** - * Wrapped updateContentSession to prevent updates after error + * Wrapped `updateContentSession` that becomes a no-op once an error has been + * surfaced, so that subsequent updates do not overwrite the errored state. + * @param {object} data - Update payload forwarded to `updateContentSession`. + * @returns {Promise} Resolves with the underlying call's result, or with + * `undefined` when the session is currently errored. */ const wrappedUpdateContentSession = data => { if (!errored.value) { @@ -102,7 +105,8 @@ export default function useCourseContentProgress({ contentNode, courseSessionId, }; /** - * Initialize the content session for progress tracking + * Initialise the content session for progress tracking. + * @param {boolean} [repeat] - When true, treat the session as a repeat attempt. */ const initSession = async (repeat = false) => { const node = contentNode.value; @@ -186,21 +190,20 @@ export default function useCourseContentProgress({ contentNode, courseSessionId, * provided by `useCourseContentProgress`. * Should be called in any child component of CourseUnitView * that needs access to progress tracking. - * - * @typedef {Object} CourseContentProgressInjectObject + * @typedef {object} CourseContentProgressInjectObject * @property {import('vue').Ref} sessionReady Whether the content session has been * initialized and is ready. * @property {import('vue').Ref} progress The current progress value (0 to 1). * @property {import('vue').Ref} time_spent The time spent on the content in seconds. - * @property {Object} extra_fields Reactive object containing extra fields from the session. + * @property {object} extra_fields Reactive object containing extra fields from the session. * @property {import('vue').Ref} pastattempts An array of past attempts for the content. * @property {import('vue').Ref} complete Whether the content is marked as * complete. * @property {import('vue').Ref} totalattempts The total number of attempts for the * content. - * @property {import('vue').Ref} context The context object containing additional + * @property {import('vue').Ref} context The context object containing additional * information about the content session. - * @property {import('vue').Ref} mastery_criterion The mastery criterion for the + * @property {import('vue').Ref} mastery_criterion The mastery criterion for the * content,if applicable. * @property {() => void} startTrackingProgress Starts the interval timer for progress tracking. * @property {() => Promise} stopTrackingProgress Stops the interval timer and saves @@ -211,14 +214,13 @@ export default function useCourseContentProgress({ contentNode, courseSessionId, * to an absolute value. * @property {(progressDelta: number) => Promise} handleAddProgress Adds a delta to the * current progress. - * @property {(contentState: Object) => Promise} handleUpdateContentState Updates the + * @property {(contentState: object) => Promise} handleUpdateContentState Updates the * content state. - * @property {(interaction: Object) => Promise} handleUpdateInteraction Updates the + * @property {(interaction: object) => Promise} handleUpdateInteraction Updates the * interaction state. - * @property {(data: any) => Promise} updateContentSession Updates the content session. + * @property {(data: object) => Promise} updateContentSession Updates the content session. * @property {(error: Error) => void} onError Handles errors by flagging the session as errored * and dispatching to the store. - * * @returns {CourseContentProgressInjectObject} An object with properties and methods for managing * the course content progress tracking. */ diff --git a/kolibri/plugins/learn/frontend/views/HomePage/index.vue b/kolibri/plugins/learn/frontend/views/HomePage/index.vue index 4f2216554e6..0956bbe3883 100644 --- a/kolibri/plugins/learn/frontend/views/HomePage/index.vue +++ b/kolibri/plugins/learn/frontend/views/HomePage/index.vue @@ -115,7 +115,7 @@ import ExploreChannels from './ExploreChannels'; /** - * Home page contains useful suggestions for a learner, e.g. their + * Home page contains useful suggestions for a learner, such as their * resources and quizzes in progress, classes, resources to explore, etc. * What sections are displayed depends on whether a learner * is signed in and also if they're a member of classes. diff --git a/kolibri/plugins/learn/frontend/views/LearningActivityBar.vue b/kolibri/plugins/learn/frontend/views/LearningActivityBar.vue index 37bf7f0ea0c..4bb09c2071e 100644 --- a/kolibri/plugins/learn/frontend/views/LearningActivityBar.vue +++ b/kolibri/plugins/learn/frontend/views/LearningActivityBar.vue @@ -265,7 +265,7 @@ /** * Is the bar used in the context of a lesson? * There are slight differences in rendering - * related to the context, e.g. action buttons labels. + * related to the context, such as action button labels. */ isLessonContext: { type: Boolean, @@ -290,8 +290,8 @@ }, /** The progress of the currently viewed content to determine - if and which progress icon should be shown (none/started/complete) - */ + if and which progress icon should be shown (none/started/complete). + */ contentProgress: { type: Number, required: false, @@ -299,16 +299,16 @@ }, /** A 1/0 Boolean check whether we should show the Coach Content icon - to be passed to the CoachContentLabel component - */ + to be passed to the CoachContentLabel component. + */ isCoachContent: { type: Number, required: false, default: 0, }, /** - The ContentNodeKinds kind of the content being viewed - */ + The ContentNodeKinds kind of the content being viewed. + */ contentKind: { type: String, required: false, @@ -316,7 +316,7 @@ }, /** Is this a practice quiz? - */ + */ isQuiz: { type: Boolean, required: false, @@ -324,23 +324,23 @@ }, /** Is the post-quiz report what is currently displayed? - */ + */ showingReportState: { type: Boolean, required: false, default: false, }, /** - Suggested time in seconds - */ + Suggested time in seconds. + */ duration: { type: Number, required: false, default: null, }, /** - Actual time spent in seconds - */ + Actual time spent in seconds. + */ timeSpent: { type: Number, required: false, @@ -348,8 +348,8 @@ }, /** A Boolean check whether we should show the Bookmark Icon - what should not happen if the user is not logged in - */ + what should not happen if the user is not logged in. + */ showBookmark: { type: Boolean, required: false, @@ -571,6 +571,7 @@ }); }, /** + * Trigger the next-steps animation; called from TopicsContentPage via $refs. * @public */ animateNextSteps() { diff --git a/kolibri/plugins/learn/frontend/views/LibraryPage/MeteredConnectionNotificationModal.vue b/kolibri/plugins/learn/frontend/views/LibraryPage/MeteredConnectionNotificationModal.vue index 6b53cc1828e..ac9bbf807cf 100644 --- a/kolibri/plugins/learn/frontend/views/LibraryPage/MeteredConnectionNotificationModal.vue +++ b/kolibri/plugins/learn/frontend/views/LibraryPage/MeteredConnectionNotificationModal.vue @@ -159,8 +159,8 @@ message: 'Allow Kolibri to use mobile data', context: 'An option that a user can select in a form', }, - /** TODO Uncomment these when strings are not frozen, then use them to fix the other TODO - * above in this file. + /* TODO Uncomment these when strings are not frozen, then use them to fix the other TODO + * above in this file. saveFailureNotification: { message: 'Settings have not been updated', diff --git a/kolibri/plugins/learn/frontend/views/ResourceLayout/index.vue b/kolibri/plugins/learn/frontend/views/ResourceLayout/index.vue index 7b24a4c81b5..1bbad7bef5c 100644 --- a/kolibri/plugins/learn/frontend/views/ResourceLayout/index.vue +++ b/kolibri/plugins/learn/frontend/views/ResourceLayout/index.vue @@ -15,22 +15,23 @@ const REQUEST_BOTTOM_BAR_KEY = 'resourceLayoutRequestBottomBar'; const REQUEST_SIDE_PANEL_FOOTER_KEY = 'resourceLayoutRequestSidePanelFooter'; - /** + /* * ResourceLayout — full-screen layout for course content viewing. * * Nested ResourceLayouts can "claim" claimable slots from their ancestor, * with deepest winning by default and focus-based switching between subtrees. * - * @slot default - Main content area - * @slot topBar - Top navigation bar content (rendered inside KToolbar; non-claimable) - * @slot sidePanel - Side panel content (claimable by nested ResourceLayouts) - * @slot bottomBar - Bottom bar content (claimable by nested ResourceLayouts) - * @slot sidePanelTopBar - Side panel header/title area (non-claimable) - * @slot sidePanelFooter - Side panel footer (claimable by nested ResourceLayouts) + * Slots: + * default - Main content area + * topBar - Top navigation bar content (rendered inside KToolbar; non-claimable) + * sidePanel - Side panel content (claimable by nested ResourceLayouts) + * bottomBar - Bottom bar content (claimable by nested ResourceLayouts) + * sidePanelTopBar - Side panel header/title area (non-claimable) + * sidePanelFooter - Side panel footer (claimable by nested ResourceLayouts) * * Exposes: - * - onSidePanelNavigation() - to be called by parent components when navigation occurs due to - * an action on the side panel, so the panel can be closed in modal mode. + * onSidePanelNavigation() - call when navigation occurs via the side panel, + * so the panel can be closed in modal mode. */ export default { name: 'ResourceLayout', diff --git a/kolibri/plugins/learn/frontend/views/ResourceLayout/useRequestableSlot.js b/kolibri/plugins/learn/frontend/views/ResourceLayout/useRequestableSlot.js index df5c2704520..5479a272594 100644 --- a/kolibri/plugins/learn/frontend/views/ResourceLayout/useRequestableSlot.js +++ b/kolibri/plugins/learn/frontend/views/ResourceLayout/useRequestableSlot.js @@ -15,12 +15,11 @@ const ACTIVATE = Symbol('activate'); * - Resolution: focused registration wins; otherwise deepest wins; * ties broken by insertion order (last wins) * - Each caller is tracked by a unique identity so sibling unmounts - * only remove their own registration - * - * @param {string} injectionKey - The provide/inject key for this slot - * @param {Object} slots - The component's slots object (from setup context) - * @param {string} slotKey - The name of the slot to manage (e.g. 'sidePanel') - * @returns {Object} + * only remove their own registration. + * @param {string} injectionKey - The provide/inject key for this slot. + * @param {object} slots - The component's slots object (from setup context). + * @param {string} slotKey - The name of the slot to manage (e.g. 'sidePanel'). + * @returns {object} * parentRequest - The injected parent request fn (null if this is root) * contentRef - Ref holding the resolved child override render function * syncRegistration - Call in render to sync own slot with parent (nested only) diff --git a/kolibri/plugins/learn/frontend/views/TopicsContentPage.vue b/kolibri/plugins/learn/frontend/views/TopicsContentPage.vue index f1ba189a569..da9459ff4bb 100644 --- a/kolibri/plugins/learn/frontend/views/TopicsContentPage.vue +++ b/kolibri/plugins/learn/frontend/views/TopicsContentPage.vue @@ -590,10 +590,11 @@ * When a lessonId is given, this method will fetch the lesson and then fetch its * content nodes. The user is guaranteed to be logged in if there is a lessonId. * - * The nodes' progresses are mapped via the useContentNodeProgress composable - * - * @modifies this.viewResourcesContents - Assigned the content nodes retrieved - * @modifies useContentNodeProgress.contentNodeProgressMap (indirectly) + * The nodes' progresses are mapped via the useContentNodeProgress composable. + * @modifies this.viewResourcesContents - Assigned the content nodes retrieved. + * @modifies useContentNodeProgress.contentNodeProgressMap (indirectly). + * @returns {Promise} Resolves once the lesson and its resources have + * been fetched and assigned. */ fetchLessonSiblings() { // Get the lesson and then assign its resources to this.viewResourcesContents @@ -613,12 +614,13 @@ * * Then it will fetch the "next folder" - which is the next content for this.content that * is a topic. - * - * @modifies this.viewResourcesContents - Sets it to the progress-mapped nodes - * @modifies this.nextFolder - Sets the value with this.content's parents next sibling folder - * if found + * @modifies this.viewResourcesContents - Sets it to the progress-mapped nodes. + * @modifies this.nextFolder - Sets the value with this.content's parent's next sibling folder + * if found. * @modifies useContentNodeProgress.contentNodeProgressMap (indirectly) if the user - * is logged in + * is logged in. + * @returns {Promise} Resolves once the parent's children and the next-folder + * pointer have been resolved (or immediately if there is no current content). */ fetchSiblings() { if (!this.content) { diff --git a/kolibri/plugins/learn/frontend/views/__tests__/library-page.spec.js b/kolibri/plugins/learn/frontend/views/__tests__/library-page.spec.js index 66d44493470..e7f24d2a2c4 100644 --- a/kolibri/plugins/learn/frontend/views/__tests__/library-page.spec.js +++ b/kolibri/plugins/learn/frontend/views/__tests__/library-page.spec.js @@ -141,7 +141,7 @@ describe('LibraryPage', () => { beforeAll(() => { useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: false })); }); - /** useBaseSearch#displayingSearchResults is falsy and there are rootNodes */ + /* useBaseSearch#displayingSearchResults is falsy and there are rootNodes */ it('displays a grid of channel cards', async () => { const wrapper = await makeWrapper(); expect(wrapper.find('[data-testid="channels"').element).toBeTruthy(); diff --git a/kolibri/plugins/learn/frontend/views/cards/AssignmentCard/index.vue b/kolibri/plugins/learn/frontend/views/cards/AssignmentCard/index.vue index c3dcb0fa0ae..90cd667a230 100644 --- a/kolibri/plugins/learn/frontend/views/cards/AssignmentCard/index.vue +++ b/kolibri/plugins/learn/frontend/views/cards/AssignmentCard/index.vue @@ -225,7 +225,7 @@ }, props: { /** - * vue-router link object + * Vue-router link object. */ to: { type: Object, diff --git a/kolibri/plugins/learn/frontend/views/cards/BaseChannelCard.vue b/kolibri/plugins/learn/frontend/views/cards/BaseChannelCard.vue index 52771ac301a..99dba0de607 100644 --- a/kolibri/plugins/learn/frontend/views/cards/BaseChannelCard.vue +++ b/kolibri/plugins/learn/frontend/views/cards/BaseChannelCard.vue @@ -64,7 +64,7 @@ required: true, }, /** - * vue-router link object + * Vue-router link object. */ to: { type: Object, diff --git a/kolibri/plugins/learn/frontend/views/cards/ResourceCard.vue b/kolibri/plugins/learn/frontend/views/cards/ResourceCard.vue index e9ddb56af31..edec0e1877f 100644 --- a/kolibri/plugins/learn/frontend/views/cards/ResourceCard.vue +++ b/kolibri/plugins/learn/frontend/views/cards/ResourceCard.vue @@ -103,7 +103,7 @@ required: true, }, /** - * vue-router link object + * Vue-router link object. */ to: { type: Object, diff --git a/kolibri/plugins/learn/frontend/views/courses/AssessmentWrapper/__tests__/AssessmentWrapper.spec.js b/kolibri/plugins/learn/frontend/views/courses/AssessmentWrapper/__tests__/AssessmentWrapper.spec.js index 47d437e1ffb..bea61b55e96 100644 --- a/kolibri/plugins/learn/frontend/views/courses/AssessmentWrapper/__tests__/AssessmentWrapper.spec.js +++ b/kolibri/plugins/learn/frontend/views/courses/AssessmentWrapper/__tests__/AssessmentWrapper.spec.js @@ -43,6 +43,15 @@ function buildProps(overrides = {}) { * * By default `checkAnswer` returns null (no answer given). * Override by providing a different stub via the `stubs` option. + * @param {object} [config] - Stub configuration. + * @param {?Function} [config.checkAnswerFn] - Replaces the default `checkAnswer` + * implementation when provided. + * @param {number} [config.availableHints] - Initial value for the stub's `availableHints` + * data property. + * @param {number} [config.totalHints] - Initial value for the stub's `totalHints` data + * property. + * @returns {object} A Vue component definition suitable for use as a Vue Testing + * Library stub. */ function makeContentViewerStub({ checkAnswerFn, availableHints = 0, totalHints = 0 } = {}) { return { @@ -91,6 +100,13 @@ function makeContentViewerStub({ checkAnswerFn, availableHints = 0, totalHints = const DefaultStub = makeContentViewerStub(); +/** + * Render `AssessmentWrapper` with merged props and the default `ContentViewer` stub. + * @param {object} [props] - Prop overrides merged on top of `buildProps` defaults. + * @param {object} [options] - Additional render options. + * @param {object} [options.stubs] - Extra stubs to merge into the render call. + * @returns {ReturnType} The Testing Library render result. + */ function renderComponent(props = {}, { stubs, ...restOptions } = {}) { const mergedProps = buildProps(props); return render(AssessmentWrapper, { @@ -107,12 +123,18 @@ function renderComponent(props = {}, { stubs, ...restOptions } = {}) { }); } -/** Helper to read the current-status text content. */ +/** + * Helper to read the current-status text content. + * @returns {string} The trimmed text inside the `current-status` test node. + */ function getStatusText() { return screen.getByTestId('current-status').textContent.trim(); } -/** Helper to get the displayed item id from the content viewer stub. */ +/** + * Helper to get the displayed item id from the content viewer stub. + * @returns {string} The trimmed text inside the `content-viewer-item-id` test node. + */ function getDisplayedItemId() { return screen.getByTestId('content-viewer-item-id').textContent.trim(); } diff --git a/kolibri/plugins/media_player/frontend/mixins/videojsButtonMixin.js b/kolibri/plugins/media_player/frontend/mixins/videojsButtonMixin.js index c7a877a601d..f36cdf267a3 100644 --- a/kolibri/plugins/media_player/frontend/mixins/videojsButtonMixin.js +++ b/kolibri/plugins/media_player/frontend/mixins/videojsButtonMixin.js @@ -1,14 +1,20 @@ import videojs from 'video.js'; /** - * @param {String} videojsComponent A string of the videojs component to extend + * Build a base class extending the named video.js menu button component, with + * additional behaviour for hiding the menu on mouseleave and dismissing it on + * outside clicks. + * @param {string} videojsComponent - The name of the videojs component to extend. + * @returns {Function} A class extending the requested videojs component. */ export default function videojsButtonMixin(videojsComponent) { return class extends videojs.getComponent(videojsComponent) { /** - * @param player - * @param options - * @param ready + * Wire up the mouseleave-to-hide behaviour and the outside-click listener used + * to dismiss the menu. + * @param {object} player - The video.js player instance. + * @param {object} [options] - Component options forwarded to videojs. + * @param {Function} [ready] - Optional ready callback forwarded to videojs. */ constructor(player, options, ready) { super(player, options, ready); @@ -29,8 +35,9 @@ export default function videojsButtonMixin(videojsComponent) { } /** - * Should build and return an instance of a Video.js Menu - * @return {Menu} + * Should build and return an instance of a Video.js Menu. The base + * implementation throws; subclasses must override. + * @throws {Error} Always — subclasses must override this method. */ buildMenu() { throw new Error('Not implemented'); @@ -38,7 +45,8 @@ export default function videojsButtonMixin(videojsComponent) { /** * @override - * @return {Menu} + * @returns {object} A configured video.js Menu, populated with items and wired up to + * the outside-click listener. */ createMenu() { if (this.items) { @@ -65,10 +73,10 @@ export default function videojsButtonMixin(videojsComponent) { } /** - * Removes class that adds specific functionality we don't want - * - * @param {String} classNames - * @return {String} + * Removes the `vjs-menu-button-popup` class that adds specific functionality + * we don't want. + * @param {string} classNames - Space-separated class string to filter. + * @returns {string} The class string without the popup class. */ removePopupClass(classNames) { return classNames.replace(/\bvjs-menu-button-popup\b/, ' '); diff --git a/kolibri/plugins/media_player/frontend/mixins/videojsMenuItemVueMixin.js b/kolibri/plugins/media_player/frontend/mixins/videojsMenuItemVueMixin.js index 8672c50cce3..63ef1b6947f 100644 --- a/kolibri/plugins/media_player/frontend/mixins/videojsMenuItemVueMixin.js +++ b/kolibri/plugins/media_player/frontend/mixins/videojsMenuItemVueMixin.js @@ -1,8 +1,5 @@ import videojsVueMixin from './videojsVueMixin'; -/** - * @param {Object} vueComponent A compiled vue component object - */ export default function videojsMenuItemVueMixin(vueComponent) { return class extends videojsVueMixin('MenuItem', vueComponent) { createVueComponent(options = {}) { diff --git a/kolibri/plugins/media_player/frontend/mixins/videojsMenuVueMixin.js b/kolibri/plugins/media_player/frontend/mixins/videojsMenuVueMixin.js index 81d3d721400..8a4e350c4f2 100644 --- a/kolibri/plugins/media_player/frontend/mixins/videojsMenuVueMixin.js +++ b/kolibri/plugins/media_player/frontend/mixins/videojsMenuVueMixin.js @@ -1,13 +1,20 @@ import videojsVueMixin from './videojsVueMixin'; /** - * @param {Object} vueComponent A compiled vue component object + * @typedef {import('video.js').default.Component} VideoJsComponent + */ + +/** + * Build a video.js Menu subclass that renders its content through a Vue component. + * @param {object} vueComponent - A compiled vue component object + * @returns {typeof VideoJsComponent} The extended Menu class */ export default function videojsMenuVueMixin(vueComponent) { return class extends videojsVueMixin('Menu', vueComponent) { /** - * @param player - * @param options + * Initialise lock and focused-child state on top of the parent Menu constructor. + * @param {VideoJsComponent} player - The video.js player instance + * @param {object} [options] - Options forwarded to the parent Menu component */ constructor(player, options) { super(player, options); @@ -19,9 +26,8 @@ export default function videojsMenuVueMixin(vueComponent) { /** * `contentEl` is used when `addItem` is called, so this allows the addition of the text track * options (the languages) in the right spot - * * @override - * @return {*|Element} + * @returns {Element} The element into which menu items should be inserted */ contentEl() { return this.getVueComponent().contentEl(); @@ -30,9 +36,8 @@ export default function videojsMenuVueMixin(vueComponent) { /** * `contentEl` is used when `addItem` is called, so this allows the addition of the text track * options (the languages) in the right spot - * * @override - * @return {*|Element} + * @returns {Element} The element into which menu items should be inserted */ get contentEl_() { return this.contentEl(); @@ -44,9 +49,8 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Override parent's method, which adds event handlers we don't want - * * @override - * @param {Component|String} item The name or instance of the item to add + * @param {VideoJsComponent | string} item - The name or instance of the item to add */ addItem(item) { this.addChild(item); @@ -54,7 +58,6 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Triggered by mouseenter of button container - * * @override */ show() { @@ -63,7 +66,6 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Triggered by mouseleave of button container - * * @override */ hide() { @@ -72,7 +74,6 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Triggered on click in ancestor - * * @override */ lockShowing() { @@ -81,7 +82,6 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Triggered on blur in ancestor - * * @override */ unlockShowing() { @@ -89,7 +89,8 @@ export default function videojsMenuVueMixin(vueComponent) { } /** - * @param {Boolean} lock Whether or not to lock it open + * Show the menu, optionally locking it open so mouseleave won't hide it. + * @param {boolean} lock - Whether or not to lock it open */ doShow(lock = false) { const component = this.getVueComponent(); @@ -108,7 +109,8 @@ export default function videojsMenuVueMixin(vueComponent) { } /** - * @param {Boolean} unlock Whether or not to unlock it if it's locked open + * Hide the menu, optionally unlocking it first if it was locked open. + * @param {boolean} unlock - Whether or not to unlock it if it's locked open */ doHide(unlock = false) { const component = this.getVueComponent(); @@ -124,6 +126,8 @@ export default function videojsMenuVueMixin(vueComponent) { /** * Called by Video.js key event handlers + * @param {number} [index] - Child index to focus; defaults to the last focused child, wraps on + * overflow */ focus(index) { const children = this.children(); diff --git a/kolibri/plugins/media_player/frontend/mixins/videojsVueMixin.js b/kolibri/plugins/media_player/frontend/mixins/videojsVueMixin.js index e7cc55022c8..e35486f9cad 100644 --- a/kolibri/plugins/media_player/frontend/mixins/videojsVueMixin.js +++ b/kolibri/plugins/media_player/frontend/mixins/videojsVueMixin.js @@ -3,8 +3,10 @@ import store from 'kolibri/store'; import videojs from 'video.js'; /** - * @param {String} videojsComponent A string of the videojs component to extend - * @param {Object} vueComponent A compiled vue component object + * Produce a video.js component class that renders a Vue component as its DOM element. + * @param {string} videojsComponent - A string of the videojs component to extend + * @param {object} vueComponent - A compiled vue component object + * @returns {Function} Subclass of the named video.js component backed by the Vue component */ export default function videojsVueMixin(videojsComponent, vueComponent) { const VideojsComponent = videojs.getComponent(videojsComponent); @@ -14,16 +16,16 @@ export default function videojsVueMixin(videojsComponent, vueComponent) { /** * This is called by video.js code that usually constructs an element, but here we'll leverage * vue by calling it manually. - * - * @return {Element} + * @returns {Element} Root DOM element from the mounted Vue component */ createEl() { return this.createVueComponent().$el; } /** - * @param {Object} [options] - * @return {VueComponent} + * Destroy any existing Vue instance and mount a fresh one, storing it on the component. + * @param {object} [options] - Extra options forwarded to the Vue component constructor + * @returns {VueComponent} The freshly mounted Vue component */ createVueComponent(options) { this.clearVueComponent(); @@ -32,7 +34,8 @@ export default function videojsVueMixin(videojsComponent, vueComponent) { } /** - * @return {VueComponent} + * Return the currently mounted Vue component, if any. + * @returns {VueComponent} The currently held Vue component, or undefined if none is mounted */ getVueComponent() { return this._vueComponent; diff --git a/kolibri/plugins/media_player/frontend/modules/captions/index.js b/kolibri/plugins/media_player/frontend/modules/captions/index.js index 70ea0ea6519..5aae5d871da 100644 --- a/kolibri/plugins/media_player/frontend/modules/captions/index.js +++ b/kolibri/plugins/media_player/frontend/modules/captions/index.js @@ -6,7 +6,9 @@ import Settings from '../../utils/settings'; const { handleSelectedLanguageChange } = videojs.getComponent('TextTrackMenuItem').prototype; /** - * @return {{captionLanguage: *, captionSubtitles: boolean, captionTranscript: boolean}} + * Build the default captions settings, seeded from the active Vue locale. + * @returns {{captionLanguage: string, captionSubtitles: boolean, captionTranscript: boolean}} + * The default captions settings object. */ const defaultSettings = () => ({ captionLanguage: vue.locale, @@ -15,16 +17,18 @@ const defaultSettings = () => ({ }); /** - * @param state - * @return {TextTrack[]} + * Resolve the captions module's track list to a plain array. + * @param {object} state - The captions module's Vuex state. + * @returns {TextTrack[]} The configured text tracks, or an empty array when none. */ const tracks = state => { return trackUtils.listToArray(state.trackList || []); }; /** - * @param state - * @return {TextTrack|null} + * Find the track matching the active captions language. + * @param {object} state - The captions module's Vuex state. + * @returns {?TextTrack} The matching track, or undefined when no track matches. */ const languageTrack = state => { return tracks(state).find(track => state.language === track.language); @@ -81,28 +85,32 @@ export default { }, getters: { /** - * @param state - * @return {string} + * The display label for the track matching the active captions language. + * @param {object} state - The captions module's Vuex state. + * @returns {string} The matching track label, or empty string when none matches. */ languageLabel(state) { const track = languageTrack(state); return track ? track.label : ''; }, /** - * @param state - * @return {TextTrack[]} + * Resolve the captions module's track list to a plain array. + * @param {object} state - The captions module's Vuex state. + * @returns {TextTrack[]} The configured text tracks. */ tracks, /** - * @param state - * @return {TextTrack} + * Find the currently enabled text track. + * @param {object} state - The captions module's Vuex state. + * @returns {?TextTrack} The enabled track, or undefined when none is enabled. */ activeTrack(state) { return tracks(state).find(track => trackUtils.isEnabled(track)); }, /** - * @param state - * @return {TextTrack|null} + * Find the track matching the active captions language. + * @param {object} state - The captions module's Vuex state. + * @returns {?TextTrack} The matching track, or undefined when no track matches. */ languageTrack, }, diff --git a/kolibri/plugins/media_player/frontend/utils/track.js b/kolibri/plugins/media_player/frontend/utils/track.js index 318d09fd923..3d258ebbcec 100644 --- a/kolibri/plugins/media_player/frontend/utils/track.js +++ b/kolibri/plugins/media_player/frontend/utils/track.js @@ -4,8 +4,9 @@ export const MODE_DISABLED = 'disabled'; export default { /** - * @param {String} mode - * @return {boolean} + * Check whether a track mode counts as enabled (showing or hidden). + * @param {string} mode - The TextTrack mode string to check + * @returns {boolean} True if the mode is showing or hidden */ isEnabledMode(mode) { return mode === MODE_SHOWING || mode === MODE_HIDDEN; @@ -14,10 +15,9 @@ export default { /** * Setting mode can cause events, which could cause loop if we don't make sure that the mode * isn't already the mode we're going to set - * - * @param {TextTrack} track - * @param {Boolean} enabled - * @param {Boolean} [hidden] + * @param {TextTrack} track - The track to update + * @param {boolean} enabled - Whether the track should be enabled + * @param {boolean} [hidden] - If enabled, whether to hide the track's cues */ setMode(track, enabled, hidden = false) { let mode = MODE_DISABLED; @@ -32,8 +32,9 @@ export default { }, /** - * @param {TextTrack} track - * @return {boolean} + * Check whether a track is currently enabled. + * @param {TextTrack} track - The track to check + * @returns {boolean} True if the track's mode is showing or hidden */ isEnabled(track) { return this.isEnabledMode(track.mode); @@ -42,9 +43,8 @@ export default { /** * Text track lists do not implement all array-like features, so this will convert it into an * array - * - * @param {TextTrackList|TextTrackCueList} list - * @return {TextTrack[]|TextTrackCue[]} + * @param {TextTrackList|TextTrackCueList} list - The list-like object to convert + * @returns {TextTrack[]|TextTrackCue[]} A plain array of the list's entries */ listToArray(list) { return Array.prototype.slice.call(list, 0); diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/SubtitlesMenuItem.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/SubtitlesMenuItem.vue index 01efa0d20b8..05d4710422e 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/SubtitlesMenuItem.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/SubtitlesMenuItem.vue @@ -29,6 +29,7 @@ methods: { ...mapActions('mediaPlayer/captions', ['toggleSubtitles']), /** + * Accessible via parent component refs. * @public */ focus() { diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/TranscriptMenuItem.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/TranscriptMenuItem.vue index 42d73283a31..bfb4c4a4aa9 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/TranscriptMenuItem.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/TranscriptMenuItem.vue @@ -34,6 +34,7 @@ methods: { ...mapActions('mediaPlayer/captions', ['toggleTranscript']), /** + * Accessible via parent component refs. * @public */ focus() { diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsButton.js b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsButton.js index f5b3f3d2686..96feb978661 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsButton.js +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsButton.js @@ -15,7 +15,7 @@ class CaptionsButton extends buttonMixin('TextTrackButton') { /** * @override - * @return {string} + * @returns {string} The CSS class for the button element. */ buildCSSClass() { return this.removePopupClass(`vjs-captions-button ${super.buildCSSClass()}`); @@ -23,7 +23,7 @@ class CaptionsButton extends buttonMixin('TextTrackButton') { /** * @override - * @return {string} + * @returns {string} The CSS class for the wrapping element. */ buildWrapperCSSClass() { return this.removePopupClass(`vjs-captions-button ${super.buildWrapperCSSClass()}`); @@ -31,7 +31,8 @@ class CaptionsButton extends buttonMixin('TextTrackButton') { /** * @override - * @returns {TranscriptMenuItem[]|SubtitlesMenuItem[]} + * @returns {Array} The menu items to display, + * or an empty list when fewer than two text tracks are available. */ createItems() { // Use logic from parent to determine if we should fill menu diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsMenu.js b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsMenu.js index 25339241dcc..d841a1f8f4a 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsMenu.js +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerCaptions/captionsMenu.js @@ -3,8 +3,8 @@ import captionsMenu from './CaptionsMenu.vue'; class CaptionsMenu extends mixin(captionsMenu) { /** - * Reset focus child to first item - * @param {Boolean} lock + * Reset focus child to first item. + * @param {boolean} [lock] - When true, lock the menu open. */ doShow(lock = false) { this.focusedChild_ = 0; diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerIndex.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerIndex.vue index 9f4e9d9be22..891ebb8d137 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerIndex.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerIndex.vue @@ -227,6 +227,8 @@ return this.transcript && !this.loading && this.captionTracks.length > 0; }, /** + * Returns the total duration of the currently loaded media in seconds. + * @returns {number} Duration in seconds. * @public */ defaultDuration() { diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/LanguagesMenuItem.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/LanguagesMenuItem.vue index d5330bdef9e..898c7e12633 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/LanguagesMenuItem.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/LanguagesMenuItem.vue @@ -40,6 +40,7 @@ methods: { ...mapActions('mediaPlayer/captions', ['setLanguage']), /** + * Accessible via parent component refs. * @public */ focus() { diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesButton.js b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesButton.js index e58edc663c3..45bf8c2f6a3 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesButton.js +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesButton.js @@ -13,9 +13,11 @@ class LanguageIcon extends vueMixin('Component', languagesIcon) {} */ class LanguagesButton extends buttonMixin('TextTrackButton') { /** - * @param player - * @param options - * @param ready + * Construct the button and inject the languages icon into its DOM, sidestepping + * video.js's built-in icon class so that we can render a Vue component. + * @param {object} player - The video.js player instance. + * @param {object} [options] - Component options forwarded to videojs. + * @param {Function} [ready] - Optional ready callback forwarded to videojs. */ constructor(player, options, ready) { super(player, options, ready); @@ -29,7 +31,7 @@ class LanguagesButton extends buttonMixin('TextTrackButton') { /** * @override - * @return {LanguagesMenu} + * @returns {LanguagesMenu} The languages menu instance attached to this button. */ buildMenu() { return new LanguagesMenu(this.player(), { @@ -48,7 +50,7 @@ class LanguagesButton extends buttonMixin('TextTrackButton') { /** * @override - * @return {string} + * @returns {string} The CSS class for the button element. */ buildCSSClass() { return this.removePopupClass(`vjs-languages-button ${super.buildCSSClass()}`); @@ -56,16 +58,16 @@ class LanguagesButton extends buttonMixin('TextTrackButton') { /** * @override - * @return {string} + * @returns {string} The CSS class for the wrapping element. */ buildWrapperCSSClass() { return this.removePopupClass(`vjs-languages-button ${super.buildWrapperCSSClass()}`); } /** - * @see https://github.com/videojs/video.js/blob/v7.4.1/src/js/control-bar/text-track-controls/text-track-button.js#L40 * @override - * @returns {LanguagesMenuItem[]} + * @returns {LanguagesMenuItem[]} One menu item per text track on the player. + * @see https://github.com/videojs/video.js/blob/v7.4.1/src/js/control-bar/text-track-controls/text-track-button.js#L40 */ createItems() { const player = this.player(); diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenu.js b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenu.js index 863870713b5..b777be62946 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenu.js +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenu.js @@ -3,8 +3,8 @@ import languagesMenu from './LanguagesMenu.vue'; class LanguagesMenu extends mixin(languagesMenu) { /** - * Set focus child to the currently selected child when about to show the menu - * @param {Boolean} lock + * Set focus child to the currently selected child when about to show the menu. + * @param {boolean} [lock] - When true, lock the menu open. */ doShow(lock = false) { this.focusedChild_ = this.children().findIndex(child => child.selected()); diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenuItem.js b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenuItem.js index ce366963c20..87a57e45482 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenuItem.js +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerLanguages/languagesMenuItem.js @@ -3,9 +3,11 @@ import languagesMenuItem from './LanguagesMenuItem.vue'; class LanguagesMenuItem extends mixin(languagesMenuItem) { /** - * @param {Player} player - * @param {Object} options - * @param {TextTrack} options.track + * Construct a single language entry, defaulting its label from the underlying text track. + * @param {object} player - The video.js player instance. + * @param {object} [options] - Configuration for this menu item, forwarded on to the + * parent class after the label is defaulted from the track. + * @param {TextTrack} options.track - The text track represented by this menu item. */ constructor(player, options = {}) { const track = options.track; @@ -17,8 +19,10 @@ class LanguagesMenuItem extends mixin(languagesMenuItem) { } /** - * @param {Object} [options] - * @return {VueComponent} + * Mount the Vue component for this menu item, passing the localised label and + * the language code as props. + * @param {object} [options] - Additional options forwarded to the parent component. + * @returns {object} The mounted Vue component instance. */ createVueComponent(options = {}) { return super.createVueComponent( @@ -35,14 +39,16 @@ class LanguagesMenuItem extends mixin(languagesMenuItem) { } /** - * @return {String} + * Localise this entry's label, using the locale configured on the player. + * @returns {string} The localised label for the menu item. */ getLabel() { return this.localize(this.options_.label); } /** - * @return {TextTrack} + * Access the text track this menu item represents. + * @returns {TextTrack} The underlying text track. */ getTrack() { return this.options_.track; diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerMenu.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerMenu.vue index efbfe003ce4..a24365af3ca 100644 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerMenu.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerMenu.vue @@ -29,27 +29,31 @@ }, methods: { /** + * Accessible via parent component refs. + * @returns {Element} The content element. * @public - * @return {Element} */ contentEl() { return this.$refs.contentEl; }, /** + * Accessible via parent component refs. * @public */ show() { this.open = true; }, /** + * Accessible via parent component refs. * @public */ hide() { this.open = false; }, /** + * Accessible via parent component refs. + * @returns {boolean} Whether the menu is shown. * @public - * @return {boolean} */ showing() { return this.open; diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/TranscriptCue.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/TranscriptCue.vue index 7b428284b35..04150d329ca 100755 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/TranscriptCue.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/TranscriptCue.vue @@ -101,24 +101,32 @@ }); }, /** + * Returns the duration of this transcript cue in seconds. + * @returns {number} Duration in seconds. * @public */ duration() { return this.cue.endTime - this.cue.startTime; }, /** + * Returns the rendered height of this cue element in pixels. + * @returns {number} Height in pixels. * @public */ height() { return this.$el.offsetHeight; }, /** + * Returns the vertical offset of this cue element from the top of its container. + * @returns {number} Vertical offset in pixels. * @public */ offsetTop() { return this.$el.offsetTop; }, /** + * Moves keyboard focus to this transcript cue element. + * @returns {void} * @public */ focus() { diff --git a/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/index.vue b/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/index.vue index 97b62243475..3cfd81ab1ad 100755 --- a/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/index.vue +++ b/kolibri/plugins/media_player/frontend/views/MediaPlayerTranscript/index.vue @@ -132,7 +132,8 @@ this.player.currentTime(cueTime + 0.01); }, /** - * @param {String} place `beginning` or `end` + * Move keyboard focus to the first or last cue in the transcript. + * @param {string} place - `beginning` or `end`. */ handleGoTo(place) { if (!this.cues.length || !Object.keys(this.$refs).length) { @@ -208,8 +209,10 @@ this.$nextTick(this.scrollThrottle); }, /** - * @param {Function} callback - * @return {Function} + * Build a reducer that walks each rendered cue and applies `callback`. + * Cues that fail to resolve, or callbacks that throw, are skipped. + * @param {Function} callback - Reducer invoked as `(reduced, cue)`. + * @returns {Function} A reducer compatible with `Array.prototype.reduce`. */ cueReduce(callback) { return (reduced, cueId) => { diff --git a/kolibri/plugins/media_player/frontend/views/mixins/MediaPlayerMenu.js b/kolibri/plugins/media_player/frontend/views/mixins/MediaPlayerMenu.js index 87d3c0a5fd9..c933124d073 100644 --- a/kolibri/plugins/media_player/frontend/views/mixins/MediaPlayerMenu.js +++ b/kolibri/plugins/media_player/frontend/views/mixins/MediaPlayerMenu.js @@ -9,27 +9,31 @@ export default { }, methods: { /** + * Forward to the underlying menu's `contentEl()` for direct DOM access. + * @returns {Element} The menu's content element. * @public - * @return {Element} */ contentEl() { return this.menu.contentEl(); }, /** + * Show the underlying menu. * @public */ show() { this.menu.show(); }, /** + * Hide the underlying menu. * @public */ hide() { this.menu.hide(); }, /** + * Whether the underlying menu is currently showing. + * @returns {boolean} True when the menu is visible. * @public - * @return {boolean} */ showing() { return this.menu.showing(); diff --git a/kolibri/plugins/pdf_viewer/frontend/utils/event_utils.js b/kolibri/plugins/pdf_viewer/frontend/utils/event_utils.js index 3866636d529..b6f3672e9e5 100644 --- a/kolibri/plugins/pdf_viewer/frontend/utils/event_utils.js +++ b/kolibri/plugins/pdf_viewer/frontend/utils/event_utils.js @@ -7,16 +7,16 @@ class EventBus { /** * Proxy to the Vue object that is the global dispatcher. - * @param {string} eventName - * @param {...any} args + * @param {string} eventName - Name of the event to emit. + * @param {...unknown} args - Payload arguments forwarded to listeners. */ emit(eventName, ...args) { this._eventDispatcher.$emit(eventName, ...args); } /** * Proxy to the Vue object that is the global dispatcher. - * @param {string} event - * @param {function} callback + * @param {string} event - Name of the event to subscribe to. + * @param {Function} callback - Listener invoked when the event fires. */ on(event, callback) { this._eventDispatcher.$on(event, callback); @@ -24,16 +24,16 @@ class EventBus { /** * Proxy to the Vue object that is the global dispatcher. * Takes any arguments and passes them on. - * @param {string} event - * @param {function} callback + * @param {string} event - Name of the event to subscribe to once. + * @param {Function} callback - Listener invoked once when the event fires. */ once(event, callback) { this._eventDispatcher.$once(event, callback); } /** * Proxy to the Vue object that is the global dispatcher. - * @param {string} event - * @param {function} callback + * @param {string} event - Name of the event to unsubscribe from. + * @param {Function} callback - The previously-subscribed listener to remove. */ off(event, callback) { this._eventDispatcher.$off(event, callback); diff --git a/kolibri/plugins/pdf_viewer/frontend/utils/text_layer_builder.js b/kolibri/plugins/pdf_viewer/frontend/utils/text_layer_builder.js index 4c9df3f6610..3d0c6436d17 100644 --- a/kolibri/plugins/pdf_viewer/frontend/utils/text_layer_builder.js +++ b/kolibri/plugins/pdf_viewer/frontend/utils/text_layer_builder.js @@ -30,14 +30,13 @@ const logging = logger.getLogger(__filename); const EXPAND_DIVS_TIMEOUT = 300; // ms /** - * @typedef {Object} TextLayerBuilderOptions - * @property {HTMLDivElement} textLayerDiv - The text layer container. - * @property {number} pageIndex - The page index. - * @property {PageViewport} viewport - The viewport of the text layer. - * @property {TextHighlighter} highlighter - Optional object that will handle - * highlighting text from the find controller. - * @property {boolean} enhanceTextSelection - Option to turn on improved - * text selection. + * @typedef {object} TextLayerBuilderOptions + * @property {HTMLDivElement} textLayerDiv - The text layer container DOM node. + * @property {number} pageIndex - Zero-based PDF page index. + * @property {object} viewport - The pdf.js PageViewport for the text layer. + * @property {?object} highlighter - Optional pdf.js TextHighlighter that will handle + * highlighting text from the find controller. + * @property {boolean} enhanceTextSelection - Option to turn on improved text selection. */ /** @@ -71,6 +70,8 @@ class TextLayerBuilder { } /** + * Mark rendering as complete and emit `textlayerrendered` once any post-render + * teardown has run. * @private */ _finishRendering() { @@ -95,7 +96,6 @@ class TextLayerBuilder { /** * Renders the text layer. - * * @param {number} [timeout] - Wait for a specified amount of milliseconds * before rendering. */ @@ -190,7 +190,6 @@ class TextLayerBuilder { * Improves text selection by adding an additional div where the mouse was * clicked. This reduces flickering of the content if the mouse is slowly * dragged up or down. - * * @private */ _bindMouse() { diff --git a/kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue b/kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue index 94db28b279f..6048ea12c6e 100644 --- a/kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue +++ b/kolibri/plugins/pdf_viewer/frontend/views/PdfRendererIndex.vue @@ -527,6 +527,8 @@ * - https://github.com/mozilla/pdf.js/blob/v2.14.305/web/pdf_link_service.js#L237 * - https://github.com/mozilla/pdf.js/blob/v2.14.305/web/pdf_link_service.js#L176 * - https://github.com/mozilla/pdf.js/blob/v2.14.305/web/base_viewer.js#L1175 + * @param {string|Array} dest - A named destination string or an explicit + * destination array as defined by the PDF specification. */ goToDestination(dest) { if (!this.pdfDocument) { @@ -602,7 +604,11 @@ ); }, /** - * Focus a given pdf page and return true if the page was already rendered + * Focus a given pdf page and return true if the page was already rendered. + * @param {number} pageNumber - 1-based index of the page to focus. + * @param {HTMLElement} bookmark - The bookmark element to return focus to + * when the user shift-enters out of the page. + * @returns {boolean} True if the page was rendered and successfully focused. */ focusPage(pageNumber, bookmark) { const page = document.querySelector('#pdf-page-' + pageNumber); @@ -630,6 +636,11 @@ * Get the page number from the explicit destination array. * Adaptation of the original function from pdf.js: * - https://github.com/mozilla/pdf.js/blob/v2.14.305/web/pdf_link_service.js#L181 + * @param {Array} explicitDest - Explicit destination array as defined by the + * PDF specification; the first element is either a page reference object or + * a numeric page index. + * @returns {Promise} Resolves to the 1-based page number, + * or undefined when it cannot be determined. */ getDestinationPageNumber(explicitDest) { return new Promise(resolve => { diff --git a/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue b/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue index d85b4a0e2ac..9cecce10bca 100644 --- a/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue +++ b/kolibri/plugins/perseus_viewer/frontend/views/PerseusRendererIndex.vue @@ -83,15 +83,17 @@ * from the same file, but with different URLs. This also allows us to only monkey patch the Util * functions once, as it gives us a global register and prevents duelling components from * overriding each other. - * - * @type { - * Object.}> - * } - * + * @type {{ + * [key: string]: { + * zipFile: ZipFile, + * usageCounter: number, + * imageUrls: {[key: string]: string}, + * }, + * }} * @property {ZipFile} zipFile - A ZipFile object for the Perseus file. * @property {number} usageCounter - The number of components using this object. - * @property {Object.} imageUrls - A lookup object mapping from the image filename - * to the URL generated for that image for display. + * @property {{[key: string]: string}} imageUrls - A lookup object mapping from the image + * filename to the URL generated for that image for display. */ const globalPerseusFileRegistry = {}; @@ -601,6 +603,10 @@ } }, /** + * Score the current answer state through the Perseus item renderer and + * return the result, or null when the renderer is not yet ready. + * @returns {?{correct: boolean, answerState: object, simpleAnswer: string}} + * The check result, or null when no answer can be checked. * @public */ checkAnswer() { @@ -640,6 +646,7 @@ } }, /** + * Reveal the next hint and emit the updated answer state. * @public */ takeHint() { diff --git a/kolibri/plugins/qti_viewer/frontend/components/AssessmentItem.vue b/kolibri/plugins/qti_viewer/frontend/components/AssessmentItem.vue index 8d3122c6efc..9b341d4c1d2 100644 --- a/kolibri/plugins/qti_viewer/frontend/components/AssessmentItem.vue +++ b/kolibri/plugins/qti_viewer/frontend/components/AssessmentItem.vue @@ -22,14 +22,13 @@ import TextEntryInteraction from './interactions/TextEntryInteraction.vue'; /** - * Extract QTI declarations of a specific type from an XML document - * @param {Document} xmlDocument - The QTI XML document - * @param {string} declarationType - 'response', 'outcome', or 'context' - * @param {Function} interactionHandler - a function that is called when a variable value is set - * @param {Ref{Object}} injectedAnswerState - a computed ref that contains any injected answers - * @returns {Object} Map of identifier -> QTIVariable + * Extract QTI declarations of a specific type from an XML document. + * @param {Document} xmlDocument - The QTI XML document. + * @param {string} declarationType - 'response', 'outcome', or 'context'. + * @param {Function} interactionHandler - A function called when a variable value is set. + * @returns {object} Map of identifier to QTIVariable. */ - function getQTIDeclarations(xmlDocument, declarationType, interactionHander) { + function getQTIDeclarations(xmlDocument, declarationType, interactionHandler) { const declarations = {}; const selector = `qti-${declarationType}-declaration`; @@ -37,7 +36,7 @@ const nodes = xmlDocument.querySelectorAll(selector); for (const node of nodes) { - const variable = new QTIVariable(node, interactionHander); + const variable = new QTIVariable(node, interactionHandler); declarations[variable.identifier] = variable; } return declarations; diff --git a/kolibri/plugins/qti_viewer/frontend/components/interactions/ChoiceInteraction.vue b/kolibri/plugins/qti_viewer/frontend/components/interactions/ChoiceInteraction.vue index 78b39bb8c94..0e09b0c2d2a 100644 --- a/kolibri/plugins/qti_viewer/frontend/components/interactions/ChoiceInteraction.vue +++ b/kolibri/plugins/qti_viewer/frontend/components/interactions/ChoiceInteraction.vue @@ -23,6 +23,9 @@ /** * Safely normalizes a response value to an array. * Handles null, undefined, scalars, and arrays uniformly. + * @param {string|number|Array|null|undefined} value - The QTI response + * value, which may be a single identifier, an array of identifiers, or empty. + * @returns {Array} An array of selected identifiers (empty when value is empty). */ function getSelectionsArray(value) { if (value === null || value === undefined) { diff --git a/kolibri/plugins/qti_viewer/frontend/utils/props.js b/kolibri/plugins/qti_viewer/frontend/utils/props.js index ebce7970bc4..fea924ff6e3 100644 --- a/kolibri/plugins/qti_viewer/frontend/utils/props.js +++ b/kolibri/plugins/qti_viewer/frontend/utils/props.js @@ -4,18 +4,18 @@ import { coerceNumber, validateNumber, validateBoolean } from './qti/values'; const QTI_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$/; /** - * Validates QTI identifier format - * @param {string|null} value - The value to validate - * @returns {boolean} - True if valid QTI identifier or null + * Validates QTI identifier format. + * @param {string|null} value - The value to validate. + * @returns {boolean} - True if valid QTI identifier or null. */ const validateQTIIdentifier = value => { return QTI_IDENTIFIER_PATTERN.test(value); }; /** - * Validates non-negative integer - * @param {string|number|null} value - The value to validate - * @returns {boolean} - True if non-negative integer or null + * Validates non-negative integer. + * @param {string|number|null} value - The value to validate. + * @returns {boolean} - True if non-negative integer or null. */ const validateNonNegativeInt = value => { value = coerceNumber(value); @@ -23,9 +23,9 @@ const validateNonNegativeInt = value => { }; /** - * Creates an enum validator function - * @param {Object} enumObject - The enum object to validate against - * @returns {Function} Validator function + * Creates an enum validator function. + * @param {object} enumObject - The enum object to validate against. + * @returns {Function} Validator function. */ const createEnumValidator = enumObject => { return value => Object.values(enumObject).includes(value); @@ -48,11 +48,11 @@ const validateOrientation = createEnumValidator(Orientation); // Common factory function for creating props /** - * Creates a Vue prop configuration from a base prop object - * @param {Object} baseProp - Base prop configuration (type, validator, etc.) - * @param {boolean} required - Whether the prop is required (default: true) - * @param {*} defaultValue - Default value (default: null when not required) - * @returns {Object} Vue prop configuration + * Creates a Vue prop configuration from a base prop object. + * @param {object} baseProp - Base prop configuration (type, validator, etc.). + * @param {boolean} required - Whether the prop is required (default: true). + * @param {unknown} defaultValue - Default value (default: null when not required). + * @returns {object} Vue prop configuration. */ const createProp = (baseProp, required = true, defaultValue) => { const prop = { ...baseProp }; diff --git a/kolibri/plugins/qti_viewer/frontend/utils/qti/values.js b/kolibri/plugins/qti_viewer/frontend/utils/qti/values.js index 570ea779a7b..4d5040dc619 100644 --- a/kolibri/plugins/qti_viewer/frontend/utils/qti/values.js +++ b/kolibri/plugins/qti_viewer/frontend/utils/qti/values.js @@ -41,9 +41,11 @@ function parseSpaceSeparated(str, coerceFn) { } /** - * Coerces a value to a QTI point (array of two integers) - * @param {*} value - The value to coerce - * @returns {Array} - [x, y] array of integers + * Coerces a value to a QTI point (array of two integers). + * @param {string|Array} value - The value to coerce; accepts an + * `[x, y]` array or a space-separated string. + * @returns {[number, number]} `[x, y]` array of integers. + * @throws {TypeError} When `value` cannot be coerced to a point. */ export function coercePoint(value) { if (isArray(value) && value.length === 2) { @@ -59,9 +61,9 @@ export function coercePoint(value) { } /** - * Validates if a value can be coerced to a QTI point - * @param {*} value - The value to validate - * @returns {boolean} - True if valid point + * Validates if a value can be coerced to a QTI point. + * @param {string|Array} value - The value to validate. + * @returns {boolean} True if valid point. */ export function validatePoint(value) { try { @@ -73,9 +75,11 @@ export function validatePoint(value) { } /** - * Coerces a value to a QTI pair (array of two strings) - * @param {*} value - The value to coerce - * @returns {Array} - [first, second] array of strings + * Coerces a value to a QTI pair (array of two strings). + * @param {string|Array} value - The value to coerce; accepts a + * `[first, second]` array or a space-separated string. + * @returns {[string, string]} `[first, second]` array of strings. + * @throws {TypeError} When `value` cannot be coerced to a pair. */ export function coercePair(value) { if (isArray(value) && value.length === 2) { @@ -91,9 +95,9 @@ export function coercePair(value) { } /** - * Validates if a value can be coerced to a QTI pair - * @param {*} value - The value to validate - * @returns {boolean} - True if valid pair + * Validates if a value can be coerced to a QTI pair. + * @param {string|Array} value - The value to validate. + * @returns {boolean} True if valid pair. */ export function validatePair(value) { try { @@ -105,9 +109,10 @@ export function validatePair(value) { } /** - * Coerces a value to a QTI duration (non-negative number) - * @param {*} value - The value to coerce - * @returns {number} - The coerced duration value + * Coerces a value to a QTI duration (non-negative number). + * @param {string|number} value - The value to coerce. + * @returns {number} The coerced duration value. + * @throws {TypeError} When `value` cannot be coerced to a non-negative number. */ export function coerceDuration(value) { const num = parseFloat(value); @@ -118,9 +123,9 @@ export function coerceDuration(value) { } /** - * Validates if a value can be coerced to a QTI duration - * @param {*} value - The value to validate - * @returns {boolean} - True if valid duration + * Validates if a value can be coerced to a QTI duration. + * @param {string|number} value - The value to validate. + * @returns {boolean} True if valid duration. */ export function validateDuration(value) { try { @@ -132,19 +137,20 @@ export function validateDuration(value) { } /** - * Validates if a value is a valid QTI file (JavaScript File object) - * @param {*} value - The value to validate - * @returns {boolean} - True if valid File object + * Validates if a value is a valid QTI file (JavaScript File object). + * @param {unknown} value - The value to validate. + * @returns {boolean} True if `value` is a `File` instance. */ export function validateFile(value) { return value instanceof File; } /** - * Coerces a value to the specified QTI base type - * @param {*} value - The value to coerce - * @param {string} baseType - The QTI base type - * @returns {*} - The coerced value + * Coerces a value to the specified QTI base type. + * @param {unknown} value - The value to coerce. + * @param {string} baseType - The QTI base type. + * @returns {unknown} The coerced value, or null when `value` is null/undefined/empty. + * @throws {TypeError} When `value` cannot be coerced to the requested base type. */ export function coerceValueWithBaseType(value, baseType) { // Handle null/undefined/empty cases per QTI specification diff --git a/kolibri/plugins/setup_wizard/frontend/api.js b/kolibri/plugins/setup_wizard/frontend/api.js index 27fa75937cb..f4a1bd9348d 100644 --- a/kolibri/plugins/setup_wizard/frontend/api.js +++ b/kolibri/plugins/setup_wizard/frontend/api.js @@ -5,8 +5,8 @@ import { Resource } from 'kolibri/apiResource'; /** * The Resource classes here map directly to the ViewSet of the same * name in the kolibri.plugins.setup_wizard.api module (note how the definitions of) - * the Resource instances below have 'kolibri.plugins.setup_wizard' for their 'namespace' - **/ + * the Resource instances below have 'kolibri.plugins.setup_wizard' for their 'namespace'. + */ export const SetupWizardResource = new Resource({ name: 'setupwizard', diff --git a/kolibri/plugins/setup_wizard/frontend/constants.js b/kolibri/plugins/setup_wizard/frontend/constants.js index f7e6138b022..8ed205a75f1 100644 --- a/kolibri/plugins/setup_wizard/frontend/constants.js +++ b/kolibri/plugins/setup_wizard/frontend/constants.js @@ -1,7 +1,7 @@ import permissionPresets from '../../../core/auth/constants/facility_configuration_presets.json'; /** - * enum identifying whether the user has gone to the on my own flow or not + * Enum identifying whether the user has gone to the on my own flow or not. */ const UsePresets = Object.freeze({ GROUP: 'group', diff --git a/kolibri/plugins/setup_wizard/frontend/machines/wizardMachine.js b/kolibri/plugins/setup_wizard/frontend/machines/wizardMachine.js index 4b501d165f2..d20a6b826ae 100644 --- a/kolibri/plugins/setup_wizard/frontend/machines/wizardMachine.js +++ b/kolibri/plugins/setup_wizard/frontend/machines/wizardMachine.js @@ -235,9 +235,9 @@ export const wizardMachine = createMachine( }, personalDataConsent: { /** - * nextEvent here is used to provide the Vue component what command it is expected to send - * in this particular case - **/ + * The `nextEvent` property specifies the event the Vue component is expected to send + * in this particular case. + */ meta: { route: { name: 'PERSONAL_DATA_CONSENT' }, nextEvent: 'CONTINUE' }, on: { CONTINUE: 'createSuperuserAndFacility', @@ -322,7 +322,7 @@ export const wizardMachine = createMachine( on: { ...importLodUsersDefinition.on, PREVIOUS_STATE: '#wizard.fullOrLearnOnlyDevice', - /** TODO Probably these two events could be refactored */ + // TODO Probably these two events could be refactored SET_SUPERUSER: { actions: 'setSuperuser' }, SET_SUPERADMIN: { actions: 'setSuperuserIfNull' }, IMPORT_USER: { actions: 'setSuperuserIfNull' }, @@ -420,9 +420,7 @@ export const wizardMachine = createMachine( }), /** * Assigns the machine to have the initial context again while maintaining the value of - * canGetOsUser. - - * This effectively resets the machine's state + * canGetOsUser. This effectively resets the machine's state. */ resetContext: assign(initialContext), setImportedFacility: assign({ diff --git a/kolibri/plugins/setup_wizard/frontend/modules/pluginModule.js b/kolibri/plugins/setup_wizard/frontend/modules/pluginModule.js index 81146d3e8e2..be13caee24a 100644 --- a/kolibri/plugins/setup_wizard/frontend/modules/pluginModule.js +++ b/kolibri/plugins/setup_wizard/frontend/modules/pluginModule.js @@ -41,28 +41,34 @@ export default { }; }, actions: { - /** -- Not used as is, but useful for posterity - setFormalUsageDefaults(store) { - const defaults = permissionPresets.formal.mappings; - store.commit('SET_FACILITY_PRESET', Presets.FORMAL); - store.commit('SET_ALLOW_GUEST_ACCESS', false); - store.commit('SET_LEARNER_CAN_SIGN_UP', defaults.learner_can_sign_up); - store.commit( - 'SET_LEARNER_CAN_LOGIN_WITH_NO_PASSWORD', - defaults.learner_can_login_with_no_password - ); - }, - setNonformalUsageDefaults(store) { - const defaults = permissionPresets.nonformal.mappings; - store.commit('SET_FACILITY_PRESET', Presets.NONFORMAL); - store.commit('SET_ALLOW_GUEST_ACCESS', true); - store.commit('SET_LEARNER_CAN_SIGN_UP', defaults.learner_can_sign_up); - store.commit( - 'SET_LEARNER_CAN_LOGIN_WITH_NO_PASSWORD', - defaults.learner_can_login_with_no_password - ); - }, - **/ + // Not used as is, but useful for posterity: + // + // setFormalUsageDefaults(store) { + // const defaults = permissionPresets.formal.mappings; + // store.commit('SET_FACILITY_PRESET', Presets.FORMAL); + // store.commit('SET_ALLOW_GUEST_ACCESS', false); + // store.commit('SET_LEARNER_CAN_SIGN_UP', defaults.learner_can_sign_up); + // store.commit( + // 'SET_LEARNER_CAN_LOGIN_WITH_NO_PASSWORD', + // defaults.learner_can_login_with_no_password, + // ); + // }, + // setNonformalUsageDefaults(store) { + // const defaults = permissionPresets.nonformal.mappings; + // store.commit('SET_FACILITY_PRESET', Presets.NONFORMAL); + // store.commit('SET_ALLOW_GUEST_ACCESS', true); + // store.commit('SET_LEARNER_CAN_SIGN_UP', defaults.learner_can_sign_up); + // store.commit( + // 'SET_LEARNER_CAN_LOGIN_WITH_NO_PASSWORD', + // defaults.learner_can_login_with_no_password, + // ); + // }, + + /** + * Mark the wizard as having errored and surface the error globally. + * @param {object} store - The Vuex action context. + * @param {object|string} errorMsg - The error to forward to `handleApiError`. + */ showError(store, errorMsg) { store.commit('SET_ERROR', true); handleApiError({ error: errorMsg }); diff --git a/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue b/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue index deabaa6eb8a..cd8699b59a3 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/ImportMultipleUsers.vue @@ -81,15 +81,16 @@ // when it is finally in QUEUED status in the task poll. const TASK_ENQUEUE_TIMEOUT = 5000; - /** Workflow + /** + Workflow - wizardService holds successfully imported learners and a list of all possible learners - This component will maintain a list of users currently being imported by polling the SoudQueue task queue - we use this list of users to change their "import" button to a circular loader; then when they are done being imported, we add them to the final state which allows us to identify them as being "imported" in place of the "import" button - If the admin goes back from here they go to a loading page which will ping the same Queue - and offer them to import another user once all SoudQueue tasks are COMPLETE -*/ + and offer them to import another user once all SoudQueue tasks are COMPLETE. + */ export default { name: 'ImportMultipleUsers', components: { diff --git a/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue b/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue index 963cd350094..d5b32fa34d7 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/LoadingTaskPage.vue @@ -168,8 +168,8 @@ pollTask() { /** - Save tasks returned to this.loadingTasks - - Clear completed - **/ + - Clear completed. + */ TaskResource.list({ queue: this.queue }) .then(tasks => { if (!tasks.length) { diff --git a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/FacilityNameTextbox.vue b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/FacilityNameTextbox.vue index 4469b39be26..d8c06f0b428 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/FacilityNameTextbox.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/FacilityNameTextbox.vue @@ -52,6 +52,7 @@ this.fieldVisited = true; }, /** + * Moves keyboard focus to the facility name input field. * @public */ focus() { diff --git a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue index 8a8a95dc7a1..3c925e23683 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/SettingUpKolibri.vue @@ -124,7 +124,7 @@ return null; // We'll set this to a key and null values are removed from the API call } }, - /** The data we will use to initialize the device during provisioning */ + /* The data we will use to initialize the device during provisioning */ deviceProvisioningData() { let superuser = null; // We need the superuser information unless the superuser will be created at login, @@ -168,7 +168,7 @@ return omitBy(payload, v => v === null); }, - /** Introspecting the machine via it's `state.context` properties */ + /* Introspecting the machine via it's `state.context` properties */ isOnMyOwnSetup() { return this.wizardContext('onMyOwnOrGroup') == Presets.PERSONAL; }, diff --git a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/UserCredentialsForm.vue b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/UserCredentialsForm.vue index a57d1f4ba24..25dba04e476 100644 --- a/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/UserCredentialsForm.vue +++ b/kolibri/plugins/setup_wizard/frontend/views/onboarding-forms/UserCredentialsForm.vue @@ -140,7 +140,7 @@ type: Boolean, default: false, }, - /** Will use learner-focused labels if false -- the data flow is the same in any case **/ + /* Will use learner-focused labels if false -- the data flow is the same in any case */ adminUserLabels: { type: Boolean, default: true, diff --git a/kolibri/plugins/user_auth/frontend/api.js b/kolibri/plugins/user_auth/frontend/api.js index b77c59a0496..6b17c83f27f 100644 --- a/kolibri/plugins/user_auth/frontend/api.js +++ b/kolibri/plugins/user_auth/frontend/api.js @@ -2,9 +2,13 @@ import urls from 'kolibri/urls'; import client from 'kolibri/client'; /** - * Sets a password that is currently not specified - * due to an account that was created while passwords - * were not required. + * Set a password for an account that was created while passwords were not + * required, and so currently has no password specified. + * @param {object} payload - Request payload. + * @param {string} payload.username - The username for the account being updated. + * @param {string} payload.password - The new password to set. + * @param {string} payload.facility - The id of the facility the account belongs to. + * @returns {Promise} Resolves with the request response. */ export function setUnspecifiedPassword({ username, password, facility }) { return client({ diff --git a/kolibri/plugins/user_auth/frontend/views/SignInPage/PictureLogin/PicturePasswordOption.vue b/kolibri/plugins/user_auth/frontend/views/SignInPage/PictureLogin/PicturePasswordOption.vue index ebfc4301620..054eba29fb3 100644 --- a/kolibri/plugins/user_auth/frontend/views/SignInPage/PictureLogin/PicturePasswordOption.vue +++ b/kolibri/plugins/user_auth/frontend/views/SignInPage/PictureLogin/PicturePasswordOption.vue @@ -174,7 +174,7 @@ required: true, }, /** - * position in the selection sequence when this option is selected + * Position in the selection sequence when this option is selected * or null when unselected. */ sequencePosition: { diff --git a/packages/eslint-plugin-kolibri/lib/index.js b/packages/eslint-plugin-kolibri/lib/index.js index 3ed1c4bd567..0ef77979b86 100644 --- a/packages/eslint-plugin-kolibri/lib/index.js +++ b/packages/eslint-plugin-kolibri/lib/index.js @@ -1,5 +1,5 @@ /** - * @fileoverview Custom rules. + * @file Custom rules. * @author Learning Equality */ 'use strict'; diff --git a/packages/eslint-plugin-kolibri/lib/rules/no-undefined-translator-keys.js b/packages/eslint-plugin-kolibri/lib/rules/no-undefined-translator-keys.js index 47c805f5fde..91897a5420f 100644 --- a/packages/eslint-plugin-kolibri/lib/rules/no-undefined-translator-keys.js +++ b/packages/eslint-plugin-kolibri/lib/rules/no-undefined-translator-keys.js @@ -1,5 +1,5 @@ /** - * @fileoverview Disallow destructuring undefined keys from createTranslator objects. + * @file Disallow destructuring undefined keys from createTranslator objects. */ 'use strict'; @@ -13,9 +13,9 @@ const espree = require('espree'); const translatorCache = new Map(); /** - * Checks if a node is a call to createTranslator - * @param {Object} node - AST node - * @returns {boolean} + * Checks if a node is a call to createTranslator. + * @param {object} node - AST node to check. + * @returns {boolean} True if the node is a createTranslator call. */ function isCreateTranslatorCall(node) { return ( @@ -27,9 +27,9 @@ function isCreateTranslatorCall(node) { } /** - * Extracts message keys from a createTranslator call's second argument - * @param {Object} node - AST CallExpression node - * @returns {Set|null} Set of message keys or null if extraction fails + * Extracts message keys from a createTranslator call's second argument. + * @param {object} node - AST CallExpression node. + * @returns {Set|null} Set of message keys or null if extraction fails. */ function extractMessageKeys(node) { if (!isCreateTranslatorCall(node)) { @@ -54,9 +54,9 @@ function extractMessageKeys(node) { } /** - * Gets the variable name that a createTranslator call is assigned to - * @param {Object} node - AST CallExpression node - * @returns {string|null} Variable name or null + * Gets the variable name that a createTranslator call is assigned to. + * @param {object} node - AST CallExpression node. + * @returns {string|null} Variable name or null. */ function getVariableName(node) { // Handle: const translator = createTranslator(...) @@ -67,9 +67,9 @@ function getVariableName(node) { } /** - * Gets the object variable name and property name for a createTranslator in an object - * @param {Object} node - AST CallExpression node - * @returns {{objectName: string, propertyName: string}|null} + * Gets the object variable name and property name for a createTranslator in an object. + * @param {object} node - AST CallExpression node. + * @returns {{objectName: string, propertyName: string}|null} Object/property name pair or null. */ function getObjectPropertyInfo(node) { // Handle: const obj = { prop: createTranslator(...) } @@ -90,9 +90,9 @@ function getObjectPropertyInfo(node) { } /** - * Gets the variable name that a createTranslator call is exported as - * @param {Object} node - AST CallExpression node - * @returns {string|null} Export name or null + * Gets the variable name that a createTranslator call is exported as. + * @param {object} node - AST CallExpression node. + * @returns {string|null} Export name or null. */ function getExportName(node) { // Handle: export const translator = createTranslator(...) @@ -108,9 +108,9 @@ function getExportName(node) { } /** - * Gets the source variable name from a destructuring assignment - * @param {Object} node - AST node (the init part of VariableDeclarator) - * @returns {string|null} Source variable name or null + * Gets the source variable name from a destructuring assignment. + * @param {object} node - AST node (the init part of VariableDeclarator). + * @returns {string|null} Source variable name or null. */ function getSourceVariableName(node) { if (node.type === 'Identifier') { @@ -120,9 +120,9 @@ function getSourceVariableName(node) { } /** - * Gets member expression information for destructuring from object.property - * @param {Object} node - AST node (the init part of VariableDeclarator) - * @returns {{objectName: string, propertyName: string}|null} + * Gets member expression information for destructuring from object.property. + * @param {object} node - AST node (the init part of VariableDeclarator). + * @returns {{objectName: string, propertyName: string}|null} Object/property name pair or null. */ function getMemberExpressionInfo(node) { // Handle: const { key$ } = obj.prop @@ -141,9 +141,9 @@ function getMemberExpressionInfo(node) { } /** - * Checks if a file path exists and is a file - * @param {string} filePath - Path to check - * @returns {boolean} + * Checks if a file path exists and is a file. + * @param {string} filePath - Path to check. + * @returns {boolean} True if the path exists and is a file. */ function isFile(filePath) { try { @@ -154,10 +154,10 @@ function isFile(filePath) { } /** - * Tries to resolve a path with various extensions - * @param {string} basePath - Base path to try - * @param {Array} extensions - Extensions to try - * @returns {string|null} Resolved path or null + * Tries to resolve a path with various extensions. + * @param {string} basePath - Base path to try. + * @param {Array} extensions - Extensions to try. + * @returns {string|null} Resolved path or null. */ function tryResolveWithExtensions(basePath, extensions) { // Try without extension first @@ -177,10 +177,10 @@ function tryResolveWithExtensions(basePath, extensions) { } /** - * Resolves an import path to an absolute file path using Node.js resolution - * @param {string} importPath - The import path (e.g., './strings' or 'kolibri/utils/i18n') - * @param {string} currentFile - The absolute path of the current file - * @returns {string|null} Absolute path to the imported file or null if not found + * Resolves an import path to an absolute file path using Node.js resolution. + * @param {string} importPath - The import path (e.g., './strings' or 'kolibri/utils/i18n'). + * @param {string} currentFile - The absolute path of the current file. + * @returns {string|null} Absolute path to the imported file or null if not found. */ function resolveImportPath(importPath, currentFile) { const currentDir = path.dirname(currentFile); @@ -202,9 +202,9 @@ function resolveImportPath(importPath, currentFile) { } /** - * Walks an AST to find all createTranslator exports and declarations - * @param {Object} ast - Parsed AST - * @returns {Map>} Map of export name to Set of message keys + * Walks an AST to find all createTranslator exports and declarations. + * @param {object} ast - Parsed AST. + * @returns {Map>} Map of export name to Set of message keys. */ function findTranslatorsInAST(ast) { const translators = new Map(); @@ -302,10 +302,10 @@ function findTranslatorsInAST(ast) { } /** - * Parses a file and extracts translator keys for a specific export - * @param {string} filePath - Absolute path to the file - * @param {string} exportedName - Name of the exported translator - * @returns {Set|null} Set of message keys or null if not found + * Parses a file and extracts translator keys for a specific export. + * @param {string} filePath - Absolute path to the file. + * @param {string} exportedName - The export identifier to look up in the file. + * @returns {Set|null} The translator's message keys, or null when parsing fails. */ function getTranslatorKeysFromFile(filePath, exportedName) { try { @@ -342,11 +342,12 @@ function getTranslatorKeysFromFile(filePath, exportedName) { } /** - * Validates destructuring against defined translator keys - * @param {Object} node - AST VariableDeclarator node - * @param {Object} translatorInfo - Object with keys: Set - * @param {Object} context - ESLint context - * @param {string} translatorName - Name of the translator variable + * Validates destructuring against defined translator keys. + * @param {object} node - AST VariableDeclarator node. + * @param {object} translatorInfo - Object with keys: Set. + * @param {object} context - ESLint context. + * @param {string} translatorName - Name of the translator variable. + * @returns {void} */ function validateDestructuring(node, translatorInfo, context, translatorName) { const { keys } = translatorInfo; diff --git a/packages/eslint-plugin-kolibri/lib/rules/tests-no-hardcoded-strings.js b/packages/eslint-plugin-kolibri/lib/rules/tests-no-hardcoded-strings.js index bb6da3feb73..c9412c99521 100644 --- a/packages/eslint-plugin-kolibri/lib/rules/tests-no-hardcoded-strings.js +++ b/packages/eslint-plugin-kolibri/lib/rules/tests-no-hardcoded-strings.js @@ -1,5 +1,5 @@ /** - * @fileoverview Don't allow hardcoded strings in Testing Library queries. + * @file Don't allow hardcoded strings in Testing Library queries. */ 'use strict'; diff --git a/packages/eslint-plugin-kolibri/lib/rules/tests-no-stubs.js b/packages/eslint-plugin-kolibri/lib/rules/tests-no-stubs.js index 5fd44ce4c07..f0ffca85797 100644 --- a/packages/eslint-plugin-kolibri/lib/rules/tests-no-stubs.js +++ b/packages/eslint-plugin-kolibri/lib/rules/tests-no-stubs.js @@ -1,5 +1,5 @@ /** - * @fileoverview Don't allow stubs in Testing Library tests. + * @file Don't allow stubs in Testing Library tests. */ 'use strict'; diff --git a/packages/eslint-plugin-kolibri/lib/rules/vue-component-block-padding.js b/packages/eslint-plugin-kolibri/lib/rules/vue-component-block-padding.js index a703760c95a..8c607b2dc55 100644 --- a/packages/eslint-plugin-kolibri/lib/rules/vue-component-block-padding.js +++ b/packages/eslint-plugin-kolibri/lib/rules/vue-component-block-padding.js @@ -1,5 +1,5 @@ /** - * @fileoverview Require padding lines between blocks + * @file Require padding lines between blocks * Vendored and modified from: * https://github.com/vuejs/eslint-plugin-vue/blob/9b55f3c18403b0a77808ba758ec3a8e72a884036/lib/rules/padding-line-between-blocks.js * Modified for two padded lines. @@ -8,9 +8,17 @@ const utils = require('eslint-plugin-vue/lib/utils'); const kolibriUtils = require('../utils'); +/** + * @typedef {import('eslint').Rule.RuleContext} RuleContext + * @typedef {import('vue-eslint-parser').AST.VElement} VElement + * @typedef {import('vue-eslint-parser').AST.Token} Token + * @typedef {import('vue-eslint-parser').AST.ESLintProgram} Program + * @typedef {import('estree').Node} ASTNode + */ + /** * Split the source code into multiple lines based on the line delimiters. - * @param {string} text Source code as a string. + * @param {string} text - Source code as a string. * @returns {string[]} Array of source code lines. */ function splitLines(text) { @@ -18,12 +26,11 @@ function splitLines(text) { } /** - * Check and report blocks. - * This autofix inserts two blank lines between the given 2 blocks. - * @param {RuleContext} context The rule context to report. - * @param {VElement} prevBlock The previous block to check. - * @param {VElement} nextBlock The next block to check. - * @param {Token[]} betweenTokens The array of tokens between blocks. + * Check and report blocks. This autofix inserts two blank lines between the given 2 blocks. + * @param {RuleContext} context - The rule context to report. + * @param {VElement} prevBlock - The previous block to check. + * @param {VElement} nextBlock - The next block to check. + * @param {Token[]} betweenTokens - The array of tokens between blocks. * @returns {void} * @private */ @@ -87,7 +94,11 @@ module.exports = { always: 'Expected two blank lines before this block.', }, }, - /** @param {RuleContext} context */ + /** + * Build the rule's visitor map for the given ESLint context. + * @param {RuleContext} context - The ESLint rule context + * @returns {object} Visitor map consumed by ESLint + */ create(context) { const parserServices = kolibriUtils.getParserServices(context); if (!parserServices || !parserServices.getDocumentFragment) { @@ -104,15 +115,18 @@ module.exports = { /** @type {Token[]} */ let tokens; /** - * @returns {VElement[]} + * Return all top-level HTML elements in the document fragment. + * @returns {VElement[]} Array of top-level VElement nodes. */ function getTopLevelHTMLElements() { return documentFragment.children.filter(utils.isVElement); } /** - * @param {VElement} prev - * @param {VElement} next + * Get the tokens and comments between two block elements. + * @param {VElement} prev - The previous element. + * @param {VElement} next - The next element. + * @returns {Token[]} Tokens between the two elements. */ function getTokenAndCommentsBetween(prev, next) { // When there is no