Enhance audience management features with new components and API integrations

- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI.
- Implemented audience invite actions including resend and revoke functionality.
- Updated audience API to support importing members and assigning courses.
- Refactored audience data handling to include status and invite management.
- Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
Rotimi Best
2026-03-22 18:42:23 +01:00
parent e9c4082f89
commit 3919ff6c2f
157 changed files with 13183 additions and 872 deletions
@@ -1,6 +1,7 @@
<script lang="ts">
import { getExerciseQuestionContractKey } from '@cio/question-types';
import type { AnswerData } from '@cio/question-types';
import type { AnswerData, ExerciseQuestionModel } from '@cio/question-types';
import type { Question } from '$features/course/types';
import { Grade, ReasonBox } from '$features/ui/question';
import { toExerciseQuestionModel } from './question-type-utils';
import { getExerciseQuestionLabels } from './question-labels';
@@ -8,15 +9,15 @@
import type { QuestionnaireMetaData } from './store';
interface Props {
questions?: any;
questions?: Question[];
questionnaireMetaData?: Partial<QuestionnaireMetaData>;
/** Per-question scores (e.g. from `questionnaireMetaData.grades` or grading modal `questionAnswerByPoint`). */
grades?: Record<string, number>;
onSubmit?: any;
onPrevious?: any;
disableGrading?: boolean;
isGradeWithAI?: boolean;
isLoading?: boolean;
reasons?: any;
/** Optional AI grading copy keyed by question id. */
reasons?: Record<string, string>;
}
let {
@@ -33,12 +34,12 @@
isGradeWithAI = false;
}
function rejectGradeSuggestion(questionId) {
function rejectGradeSuggestion(questionId: Question['id']) {
isGradeWithAI = false;
grades[questionId] = 0;
grades[String(questionId)] = 0;
}
function getAnswerForQuestion(questionModel, fallbackIndex = 0) {
function getAnswerForQuestion(questionModel: ExerciseQuestionModel, fallbackIndex = 0): AnswerData | undefined {
const questionKey = getExerciseQuestionContractKey(questionModel, fallbackIndex);
return _questionnaireMetaData?.answers?.[questionKey];
}
@@ -59,11 +60,11 @@
{#if disableGrading && !Object.keys(grades || {}).length}
<ExerciseQuestion.QuestionList
contract={{ mode: 'view', questions: questionModels, answersByKey, labels: questionLabels, disabled: true }}
contract={{ mode: 'review', questions: questionModels, answersByKey, labels: questionLabels, disabled: true }}
itemClass="mb-4"
/>
{:else if disableGrading && Object.keys(grades || {}).length}
{#each questions as currentQuestion, index}
{#each questions as currentQuestion, index (currentQuestion.id)}
{@const questionModel = questionModels[index]}
<div class="mb-4 space-y-3">
<div class="flex items-center justify-between">
@@ -73,7 +74,7 @@
<ExerciseQuestion.QuestionRenderer
contract={{
mode: 'view',
mode: 'review',
question: questionModel,
answer: getAnswerForQuestion(questionModel, index),
labels: questionLabels,
@@ -83,7 +84,7 @@
</div>
{/each}
{:else}
{#each questions as currentQuestion, index}
{#each questions as currentQuestion, index (currentQuestion.id)}
{@const questionModel = questionModels[index]}
<div class="mb-4 space-y-3">
<div class="flex items-center justify-between">
@@ -93,7 +94,7 @@
<ExerciseQuestion.QuestionRenderer
contract={{
mode: 'view',
mode: 'review',
question: questionModel,
answer: getAnswerForQuestion(questionModel, index),
labels: questionLabels,
@@ -18,6 +18,9 @@ export function getExerciseQuestionLabels(): ExerciseQuestionLabels {
'common.correct_badge': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.common.correct_badge'
),
'common.incorrect_badge': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.common.incorrect_badge'
),
'common.not_set': t.get('course.navItem.lessons.exercises.all_exercises.shared_question.common.not_set'),
'navigation.previous': t.get('course.navItem.lessons.exercises.all_exercises.shared_question.navigation.previous'),
'navigation.next': t.get('course.navItem.lessons.exercises.all_exercises.shared_question.navigation.next'),
@@ -131,6 +134,12 @@ export function getExerciseQuestionLabels(): ExerciseQuestionLabels {
'short_answer.preview.helper': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.short_answer.preview.helper'
),
'short_answer.review.your_answer_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.short_answer.review.your_answer_label'
),
'short_answer.review.accepted_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.short_answer.review.accepted_label'
),
'numeric.edit.correct_value_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.numeric.edit.correct_value_label'
),
@@ -158,6 +167,15 @@ export function getExerciseQuestionLabels(): ExerciseQuestionLabels {
'numeric.preview.tolerance_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.numeric.preview.tolerance_label'
),
'numeric.review.your_answer_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.numeric.review.your_answer_label'
),
'numeric.review.in_range': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.numeric.review.in_range'
),
'numeric.review.out_of_range': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.numeric.review.out_of_range'
),
'fill_blank.edit.accepted_answers_placeholder': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.fill_blank.edit.accepted_answers_placeholder'
),
@@ -167,6 +185,9 @@ export function getExerciseQuestionLabels(): ExerciseQuestionLabels {
'fill_blank.preview.accepted_answers_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.fill_blank.preview.accepted_answers_label'
),
'fill_blank.review.your_answer_label': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.fill_blank.review.your_answer_label'
),
'file_upload.edit.accepted_types_placeholder': t.get(
'course.navItem.lessons.exercises.all_exercises.shared_question.file_upload.edit.accepted_types_placeholder'
),
@@ -81,7 +81,7 @@
<div class="pb-4">
<ExerciseQuestion.QuestionRenderer
contract={{
mode: 'view',
mode: 'review',
question: { ...toExerciseQuestionModel(q), title: `${i + 1}. ${q.title}` },
answer: getStudentAnswerForQuestion($submissions[studentSelected], q),
labels: questionLabels,