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:
+60
-12
@@ -29,19 +29,31 @@
|
||||
|
||||
<Story name="Checkbox">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={CHECKBOX_FIXTURE.question} answer={CHECKBOX_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={CHECKBOX_FIXTURE.question}
|
||||
answer={CHECKBOX_FIXTURE.answer}
|
||||
wrongAnswer={CHECKBOX_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="FileUpload">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={FILE_UPLOAD_FIXTURE.question} answer={FILE_UPLOAD_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={FILE_UPLOAD_FIXTURE.question}
|
||||
answer={FILE_UPLOAD_FIXTURE.answer}
|
||||
wrongAnswer={FILE_UPLOAD_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="FillBlank">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={FILL_BLANK_FIXTURE.question} answer={FILL_BLANK_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={FILL_BLANK_FIXTURE.question}
|
||||
answer={FILL_BLANK_FIXTURE.answer}
|
||||
wrongAnswer={FILL_BLANK_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -62,13 +74,21 @@
|
||||
>
|
||||
<strong>Hidden – still in development.</strong> Not available in the exercise editor.
|
||||
</div>
|
||||
<QuestionTypeModes question={HOTSPOT_FIXTURE.question} answer={HOTSPOT_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={HOTSPOT_FIXTURE.question}
|
||||
answer={HOTSPOT_FIXTURE.answer}
|
||||
wrongAnswer={HOTSPOT_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Link">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={LINK_FIXTURE.question} answer={LINK_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={LINK_FIXTURE.question}
|
||||
answer={LINK_FIXTURE.answer}
|
||||
wrongAnswer={LINK_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -89,42 +109,70 @@
|
||||
>
|
||||
<strong>Hidden – still in development.</strong> Not available in the exercise editor.
|
||||
</div>
|
||||
<QuestionTypeModes question={MATCHING_FIXTURE.question} answer={MATCHING_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={MATCHING_FIXTURE.question}
|
||||
answer={MATCHING_FIXTURE.answer}
|
||||
wrongAnswer={MATCHING_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Numeric">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={NUMERIC_FIXTURE.question} answer={NUMERIC_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={NUMERIC_FIXTURE.question}
|
||||
answer={NUMERIC_FIXTURE.answer}
|
||||
wrongAnswer={NUMERIC_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Ordering">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={ORDERING_FIXTURE.question} answer={ORDERING_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={ORDERING_FIXTURE.question}
|
||||
answer={ORDERING_FIXTURE.answer}
|
||||
wrongAnswer={ORDERING_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Radio">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={RADIO_FIXTURE.question} answer={RADIO_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={RADIO_FIXTURE.question}
|
||||
answer={RADIO_FIXTURE.answer}
|
||||
wrongAnswer={RADIO_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="ShortAnswer">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={SHORT_ANSWER_FIXTURE.question} answer={SHORT_ANSWER_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={SHORT_ANSWER_FIXTURE.question}
|
||||
answer={SHORT_ANSWER_FIXTURE.answer}
|
||||
wrongAnswer={SHORT_ANSWER_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Textarea">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={TEXTAREA_FIXTURE.question} answer={TEXTAREA_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={TEXTAREA_FIXTURE.question}
|
||||
answer={TEXTAREA_FIXTURE.answer}
|
||||
wrongAnswer={TEXTAREA_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="TrueFalse">
|
||||
{#snippet template()}
|
||||
<QuestionTypeModes question={TRUE_FALSE_FIXTURE.question} answer={TRUE_FALSE_FIXTURE.answer} />
|
||||
<QuestionTypeModes
|
||||
question={TRUE_FALSE_FIXTURE.question}
|
||||
answer={TRUE_FALSE_FIXTURE.answer}
|
||||
wrongAnswer={TRUE_FALSE_FIXTURE.wrongAnswer}
|
||||
/>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export interface QuestionStoryFixture {
|
||||
question: Record<string, unknown>;
|
||||
/** Correct answer (`AnswerData` or legacy shapes supported by renderers). */
|
||||
answer?: unknown;
|
||||
/** Incorrect answer for Review Mode demos in Storybook (`AnswerData`). */
|
||||
wrongAnswer?: unknown;
|
||||
}
|
||||
|
||||
const OPTION_IMAGE_A = 'https://brand.cdn.clsrio.com/classroomio-lms-courses.png';
|
||||
@@ -33,7 +36,8 @@ export const RADIO_FIXTURE: QuestionStoryFixture = {
|
||||
{ id: 1003, label: 'Ruby', value: 'ruby' }
|
||||
]
|
||||
},
|
||||
answer: 1001
|
||||
answer: { type: 'RADIO', optionId: 1001 },
|
||||
wrongAnswer: { type: 'RADIO', optionId: 1002 }
|
||||
};
|
||||
|
||||
export const CHECKBOX_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -63,7 +67,8 @@ export const CHECKBOX_FIXTURE: QuestionStoryFixture = {
|
||||
}
|
||||
]
|
||||
},
|
||||
answer: ['2001', '2003']
|
||||
answer: { type: 'CHECKBOX', optionIds: [2001, 2003] },
|
||||
wrongAnswer: { type: 'CHECKBOX', optionIds: [2002] }
|
||||
};
|
||||
|
||||
export const TEXTAREA_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -93,7 +98,8 @@ export const TRUE_FALSE_FIXTURE: QuestionStoryFixture = {
|
||||
correctValue: true
|
||||
}
|
||||
},
|
||||
answer: true
|
||||
answer: { type: 'TRUE_FALSE', value: true },
|
||||
wrongAnswer: { type: 'TRUE_FALSE', value: false }
|
||||
};
|
||||
|
||||
export const SHORT_ANSWER_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -103,10 +109,12 @@ export const SHORT_ANSWER_FIXTURE: QuestionStoryFixture = {
|
||||
title: 'Name one strategy for reducing flaky frontend tests. [SHORT_ANSWER]',
|
||||
questionType: 'SHORT_ANSWER',
|
||||
settings: {
|
||||
instructions: 'Answer in one sentence.'
|
||||
instructions: 'Answer in one sentence.',
|
||||
acceptedAnswers: 'use deterministic test fixtures'
|
||||
}
|
||||
},
|
||||
answer: 'Use deterministic test fixtures'
|
||||
answer: { type: 'SHORT_ANSWER', text: 'Use deterministic test fixtures' },
|
||||
wrongAnswer: { type: 'SHORT_ANSWER', text: 'Guess randomly every time' }
|
||||
};
|
||||
|
||||
export const NUMERIC_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -117,7 +125,8 @@ export const NUMERIC_FIXTURE: QuestionStoryFixture = {
|
||||
questionType: 'NUMERIC',
|
||||
settings: { correctValue: 90, tolerance: 0 }
|
||||
},
|
||||
answer: 90
|
||||
answer: { type: 'NUMERIC', value: 90 },
|
||||
wrongAnswer: { type: 'NUMERIC', value: 42 }
|
||||
};
|
||||
|
||||
export const FILL_BLANK_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -128,7 +137,8 @@ export const FILL_BLANK_FIXTURE: QuestionStoryFixture = {
|
||||
questionType: 'FILL_BLANK',
|
||||
settings: { acceptedAnswers: 'API,endpoint' }
|
||||
},
|
||||
answer: ['API']
|
||||
answer: { type: 'FILL_BLANK', values: ['API'] },
|
||||
wrongAnswer: { type: 'FILL_BLANK', values: ['SOAP'] }
|
||||
};
|
||||
|
||||
export const FILE_UPLOAD_FIXTURE: QuestionStoryFixture = {
|
||||
@@ -180,7 +190,8 @@ export const ORDERING_FIXTURE: QuestionStoryFixture = {
|
||||
items: ['Design schema', 'Implement renderer', 'Ship to production']
|
||||
}
|
||||
},
|
||||
answer: ['order-1', 'order-2', 'order-3']
|
||||
answer: { type: 'ORDERING', orderedValues: ['order-1', 'order-2', 'order-3'] },
|
||||
wrongAnswer: { type: 'ORDERING', orderedValues: ['order-3', 'order-1', 'order-2'] }
|
||||
};
|
||||
|
||||
export const HOTSPOT_FIXTURE: QuestionStoryFixture = {
|
||||
|
||||
@@ -9,7 +9,8 @@ export const QUESTION_LABELS = {
|
||||
'common.option_prefix': 'Option',
|
||||
'common.remove': 'Remove',
|
||||
'common.add_option': 'Add option',
|
||||
'common.correct_badge': '(Correct)',
|
||||
'common.correct_badge': 'Correct',
|
||||
'common.incorrect_badge': 'Incorrect',
|
||||
'common.not_set': 'Not set',
|
||||
'navigation.previous': 'Previous',
|
||||
'navigation.next': 'Next',
|
||||
@@ -51,6 +52,8 @@ export const QUESTION_LABELS = {
|
||||
'short_answer.edit.instructions_placeholder': 'Optional instructions',
|
||||
'short_answer.take.placeholder': 'Type your answer',
|
||||
'short_answer.preview.helper': 'Free-text answer question.',
|
||||
'short_answer.review.your_answer_label': 'Submitted:',
|
||||
'short_answer.review.accepted_label': 'Accepted answers',
|
||||
'numeric.edit.correct_value_label': 'Correct value',
|
||||
'numeric.edit.correct_value_info': 'Expected numeric answer to mark as correct.',
|
||||
'numeric.edit.tolerance_label': 'Tolerance',
|
||||
@@ -60,9 +63,13 @@ export const QUESTION_LABELS = {
|
||||
'numeric.take.placeholder': 'Enter a number',
|
||||
'numeric.preview.correct_value_label': 'Correct value',
|
||||
'numeric.preview.tolerance_label': 'Tolerance',
|
||||
'numeric.review.your_answer_label': 'Submitted:',
|
||||
'numeric.review.in_range': 'Within tolerance',
|
||||
'numeric.review.out_of_range': 'Outside tolerance',
|
||||
'fill_blank.edit.accepted_answers_placeholder': 'Accepted answers (comma separated)',
|
||||
'fill_blank.take.placeholder': 'Enter answers separated by commas',
|
||||
'fill_blank.preview.accepted_answers_label': 'Accepted answers',
|
||||
'fill_blank.review.your_answer_label': 'Submitted:',
|
||||
'file_upload.edit.accepted_types_placeholder': 'Select accepted file formats',
|
||||
'file_upload.edit.max_size_placeholder': 'Max size (MB)',
|
||||
'file_upload.take.selected_file_label': 'Selected file',
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
interface Props {
|
||||
question: Record;
|
||||
answer?: unknown;
|
||||
/** When set, renders an additional Review block with this incorrect `AnswerData`. */
|
||||
wrongAnswer?: unknown;
|
||||
}
|
||||
|
||||
type StoryQuestion = {
|
||||
@@ -15,7 +17,7 @@
|
||||
settings?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
let { question, answer = null }: Props = $props();
|
||||
let { question, answer = null, wrongAnswer = undefined }: Props = $props();
|
||||
|
||||
function cloneQuestion(source: Record) {
|
||||
return JSON.parse(JSON.stringify(source));
|
||||
@@ -178,6 +180,42 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ui:rounded-md ui:border ui:p-4 ui:space-y-3">
|
||||
<p class="ui:text-sm ui:font-semibold">Review Mode</p>
|
||||
<p class="ui:text-muted-foreground ui:text-xs">
|
||||
Read-only submission replay with correctness (same as grading / finished exercise in the app).
|
||||
</p>
|
||||
<ExerciseQuestion.QuestionRenderer
|
||||
showContainer={false}
|
||||
contract={{
|
||||
mode: 'review',
|
||||
question,
|
||||
answer: viewAnswer,
|
||||
labels: QUESTION_LABELS,
|
||||
disabled: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if wrongAnswer !== undefined}
|
||||
<div class="ui:rounded-md ui:border ui:p-4 ui:space-y-3">
|
||||
<p class="ui:text-sm ui:font-semibold">Review Mode (incorrect)</p>
|
||||
<p class="ui:text-muted-foreground ui:text-xs">
|
||||
Same review UI when the submitted answer does not match the correct solution.
|
||||
</p>
|
||||
<ExerciseQuestion.QuestionRenderer
|
||||
showContainer={false}
|
||||
contract={{
|
||||
mode: 'review',
|
||||
question,
|
||||
answer: wrongAnswer,
|
||||
labels: QUESTION_LABELS,
|
||||
disabled: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="ui:rounded-md ui:border ui:p-4 ui:space-y-3">
|
||||
<p class="ui:text-sm ui:font-semibold">Submission Mode (12 respondents)</p>
|
||||
<ExerciseQuestion.QuestionRenderer
|
||||
|
||||
Reference in New Issue
Block a user