-
-
Notifications
You must be signed in to change notification settings - Fork 304
feat: add AnswerSettings and QuestionSettingsHeader components #6059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0753e19
99773af
5030c92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,16 +7,25 @@ | |
| > | ||
| {{ parseError }} | ||
| </p> | ||
| <component | ||
| :is="descriptor.editorComponent" | ||
| v-else | ||
| :key="descriptor.type" | ||
| :questionType="questionType" | ||
| :interaction="interaction" | ||
| :mode="mode" | ||
| :showAnswers="showAnswers" | ||
| @update:interaction="interaction => $emit('update:interaction', interaction)" | ||
| /> | ||
| <div v-else> | ||
| <QuestionTypeSelector | ||
|
AlexVelezLl marked this conversation as resolved.
|
||
| v-if="mode === 'edit'" | ||
| :questionType="questionType" | ||
| :settingsTargetId="settingsTargetId" | ||
| @update:questionType="onUpdateQuestionType" | ||
| /> | ||
|
|
||
| <component | ||
| :is="descriptor.editorComponent" | ||
| :key="descriptor.type" | ||
| :questionType="questionType" | ||
| :interaction="interaction" | ||
| :mode="mode" | ||
| :showAnswers="showAnswers" | ||
| :teleportTargetId="settingsTargetId" | ||
|
AlexVelezLl marked this conversation as resolved.
AlexVelezLl marked this conversation as resolved.
|
||
| @update:interaction="onUpdateInteraction" | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| </template> | ||
|
|
@@ -26,10 +35,17 @@ | |
|
|
||
| import { computed, watch } from 'vue'; | ||
| import useInteractionDescriptor from '../../composables/useInteractionDescriptor'; | ||
| import QuestionTypeSelector from '../QuestionTypeSelector/index.vue'; | ||
| import { generateRandomSlug } from '../../utils/generateRandomSlug'; | ||
| import { descriptors } from '../../interactions'; | ||
|
|
||
| export default { | ||
| name: 'InteractionSection', | ||
|
|
||
| components: { | ||
| QuestionTypeSelector, | ||
| }, | ||
|
|
||
| setup(props, { emit }) { | ||
| const interactionRef = computed(() => props.interaction); | ||
| const { descriptor, questionType, parseError } = useInteractionDescriptor(interactionRef); | ||
|
|
@@ -42,7 +58,37 @@ | |
| { immediate: true }, | ||
| ); | ||
|
|
||
| return { descriptor, questionType, parseError }; | ||
| const onUpdateQuestionType = newType => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. suggestion: untested, and the only path that can lose authored content — |
||
| const newDescriptor = descriptors.find(d => d.questionTypes.includes(newType)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: this is exactly |
||
| if (newDescriptor && newDescriptor !== descriptor.value) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. blocking:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it should be upto user which option to mark as only correct option or we just take option randomly ? @AlexVelezLl
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not random — deterministic, and the UI already makes the pick. The banner does fire ( Clearing every
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll handle this in a follow-up! |
||
| const oldState = descriptor.value.parse( | ||
| props.interaction.bodyXml, | ||
| props.interaction.responseDeclarations, | ||
| ); | ||
| const freshState = newDescriptor.parse('', []); | ||
| const withPrompt = { ...freshState, prompt: oldState.prompt ?? '' }; | ||
| const newInteraction = newDescriptor.buildXML(withPrompt, newType); | ||
| emit('update:interaction', newInteraction); | ||
| } | ||
|
|
||
| questionType.value = newType; | ||
| emit('update:questionType', newType); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: line 74 already triggers the |
||
| }; | ||
|
|
||
| const onUpdateInteraction = updatedInteraction => { | ||
| emit('update:interaction', updatedInteraction); | ||
| }; | ||
|
|
||
| const settingsTargetId = generateRandomSlug('answer-settings'); | ||
|
|
||
| return { | ||
| descriptor, | ||
| questionType, | ||
| parseError, | ||
| onUpdateQuestionType, | ||
| onUpdateInteraction, | ||
| settingsTargetId, | ||
| }; | ||
| }, | ||
|
|
||
| props: { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { render, screen, fireEvent, within } from '@testing-library/vue'; | ||
| import VueRouter from 'vue-router'; | ||
| import QuestionTypeSelector from '../index.vue'; | ||
| import { QuestionType } from '../../../constants'; | ||
| import { qtiEditorStrings as tr } from '../../../qtiEditorStrings'; | ||
|
|
||
| const defaultProps = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. suggestion: Also, |
||
| questionType: QuestionType.SINGLE_SELECT, | ||
| settingsTargetId: 'test-settings-target', | ||
| }; | ||
|
|
||
| const renderHeader = (props = {}) => | ||
| render(QuestionTypeSelector, { | ||
| props: { ...defaultProps, ...props }, | ||
| routes: new VueRouter(), | ||
| }); | ||
|
|
||
| describe('QuestionTypeSelector', () => { | ||
| it('renders the type meta-label in edit mode', () => { | ||
| renderHeader(); | ||
| expect(screen.getByText(tr.$tr('typeLabel'))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders a KSelect with the selected option label (not raw enum)', () => { | ||
| renderHeader(); | ||
| expect(screen.getByText(tr.$tr('singleSelectLabel'))).toBeInTheDocument(); | ||
| expect(screen.queryByText(QuestionType.SINGLE_SELECT)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders the selected type label inside the type group', () => { | ||
| renderHeader(); | ||
| const group = screen.getByRole('group', { name: tr.$tr('typeLabel') }); | ||
| expect(within(group).getByText(tr.$tr('singleSelectLabel'))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('opens type info modal when info button clicked', async () => { | ||
| renderHeader(); | ||
|
|
||
| const helpButton = screen.getByRole('button', { name: tr.$tr('responseTypeInfoTitle') }); | ||
| await fireEvent.click(helpButton); | ||
|
|
||
| expect(screen.getByRole('dialog')).toBeInTheDocument(); | ||
| expect(screen.getByText(tr.$tr('singleChoiceDescription'))).toBeInTheDocument(); | ||
| expect(screen.getByText(tr.$tr('multipleSelectionDescription'))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('closes type info modal when Close button clicked', async () => { | ||
| renderHeader(); | ||
|
|
||
| await fireEvent.click(screen.getByRole('button', { name: tr.$tr('responseTypeInfoTitle') })); | ||
| expect(screen.getByRole('dialog')).toBeInTheDocument(); | ||
|
|
||
| await fireEvent.click(screen.getByRole('button', { name: tr.$tr('closeBtnLabel') })); | ||
| expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('emits update:questionType when a new type is selected', async () => { | ||
| const { emitted } = renderHeader(); | ||
|
|
||
| // Click the currently selected option to open the dropdown | ||
| await fireEvent.click(screen.getByText(tr.$tr('singleSelectLabel'))); | ||
|
|
||
| // Click the new option from the dropdown menu | ||
| await fireEvent.click(screen.getByText(tr.$tr('multiSelectLabel'))); | ||
|
|
||
| expect(emitted()['update:questionType']).toBeTruthy(); | ||
| expect(emitted()['update:questionType'][0]).toEqual([QuestionType.MULTI_SELECT]); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Resolved — addressed in the current code.
nitpick:
InteractionSectiongeneratessettingsTargetIdinternally and declares notargetIdprop — this looks like a leftover from an earlier iteration, and lands as a stray DOM attribute.