-
Notifications
You must be signed in to change notification settings - Fork 10
Refactor: Choice Answer #286
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
Open
lebalz
wants to merge
37
commits into
feature/choice-answer
Choose a base branch
from
refactor-attempt/choice-answer
base: feature/choice-answer
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
1e84109
move ChoiceAnswer model to folder
lebalz 519535a
add some basic tests
lebalz 455f6b3
Merge branch 'main' into refactor-attempt/choice-answer
lebalz 7ee0664
rewrite remark-transformer
lebalz cae081d
Add new interface Props to Quiz component
lebalz 221e97d
Refactor ChoiceAnswer to Quiz/ChoiceAnswer structure and update relat…
lebalz f5ed8fe
fix typo
lebalz db52758
some progress
lebalz f0507f1
remove redundant inQuiz prop
lebalz 2b8924a
setup shuffling
lebalz 2ae412a
finalize single ChoiceAnswers
lebalz f52e23a
refactor: simplify correctness handling in FeedbackBadge and iAssessable
lebalz 46b4bc0
refactor: update imports to use Assessable namespace for ChoiceAnswer…
lebalz a9e52cb
refactor: remove unused assessment logic and streamline scoring inter…
lebalz bf666d1
rename ChoiceAnswer to Assessable
lebalz a728ea3
first draft of TrueFalseAnswer
lebalz ebfe072
refactor: integrate QuestionCard component and streamline ChoiceAnswe…
lebalz 286c467
ensure TrueFalseAnswer has its own model type
lebalz 151f02c
setup quiz model
lebalz 7a311cf
finalize quiz
lebalz b21b350
cleanup visual glitches
lebalz 2899593
refactor feedback
lebalz b9fc366
make remark plugin configurable
lebalz ffa5ccc
rename to assessable components
lebalz 7aa1c56
debounce only by 50 ms
lebalz 3ee8e90
add sync state
lebalz 1f48674
indicate unanswered questions
lebalz dee3c42
make questions taskable
lebalz 86d8af7
add scroll to animations in taskable overview
lebalz 7b2edc3
remove browser check
lebalz ec47faa
fix loading document roots issues
lebalz 9eb5570
streamline color hints
lebalz 976fddf
ignore comments for ChoiceAnswer.Options
lebalz a618a9d
use standard css variables
lebalz b25a80c
Merge branch 'feature/choice-answer' into refactor-attempt/choice-answer
lebalz 94475f5
cleanup comments
lebalz 3a09b8c
fix
lebalz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,11 +19,10 @@ import iDocumentContainer from '@tdev-models/iDocumentContainer'; | |
| import iViewStore from '@tdev-stores/ViewStores/iViewStore'; | ||
| import Code from '@tdev-models/documents/Code'; | ||
| import { iTaskableDocument } from '@tdev-models/iTaskableDocument'; | ||
| import ChoiceAnswer, { | ||
| ChoiceAnswerChoices, | ||
| ChoiceAnswerOptionOrders, | ||
| ChoiceAnswerQuestionOrder | ||
| } from '@tdev-models/documents/ChoiceAnswer'; | ||
| import type ChoiceAnswer from '@tdev-models/documents/Assessable/ChoiceAnswer'; | ||
| import type TrueFalseAnswer from '@tdev-models/documents/Assessable/TrueFalseAnswer'; | ||
| import type Quiz from '@tdev-models/documents/Assessable/Quiz'; | ||
| import iAssessable from '@tdev-models/documents/Assessable/iAssessable'; | ||
|
|
||
| export enum Access { | ||
| RO_DocumentRoot = 'RO_DocumentRoot', | ||
|
|
@@ -46,11 +45,22 @@ export interface StringData { | |
| text: string; | ||
| } | ||
|
|
||
| export interface ChoiceAnswerData { | ||
| choices: ChoiceAnswerChoices; | ||
| optionOrders: ChoiceAnswerOptionOrders; | ||
| questionOrder: ChoiceAnswerQuestionOrder | null; | ||
| interface AssessableData { | ||
| assessed: boolean; | ||
| qid?: string; | ||
|
Comment on lines
+48
to
+50
Contributor
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. Generic payload for all answer types which can be assessed |
||
| } | ||
|
|
||
| export interface TrueFalseAnswerData extends AssessableData { | ||
| value: boolean | null; | ||
| } | ||
|
|
||
| export interface ChoiceAnswerData extends AssessableData { | ||
| choices: number[]; | ||
| optionsOrder: number[]; | ||
| } | ||
|
|
||
| export interface QuizData extends AssessableData { | ||
| questionOrder: number[]; | ||
| } | ||
|
|
||
| export interface QuillV2Data { | ||
|
|
@@ -122,11 +132,17 @@ export interface ViewStoreTypeMapping { | |
| export type ViewStoreType = keyof ViewStoreTypeMapping; | ||
| export type ViewStore = ViewStoreTypeMapping[ViewStoreType]; | ||
|
|
||
| export interface AssessableDataMapping { | ||
| ['true_false_answer']: TrueFalseAnswerData; | ||
| ['choice_answer']: ChoiceAnswerData; | ||
| ['quiz']: QuizData; | ||
| } | ||
|
|
||
| export interface ContainerTypeDataMapping { | ||
| ['_container_placeholder_']: { name: string }; // placeholder to avoid empty interface error | ||
| } | ||
|
|
||
| export interface TaskableDocumentMapping { | ||
| export interface TaskableDocumentMapping extends AssessableDataMapping { | ||
| ['task_state']: TaskStateData; | ||
| ['progress_state']: ProgressStateData; | ||
| } | ||
|
|
@@ -136,7 +152,6 @@ export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeD | |
| // TODO: rename to `code_version`? | ||
| ['script_version']: ScriptVersionData; | ||
| ['string']: StringData; | ||
| ['choice_answer']: ChoiceAnswerData; | ||
| ['quill_v2']: QuillV2Data; | ||
| ['solution']: SolutionData; | ||
| ['dir']: DirData; | ||
|
|
@@ -149,6 +164,7 @@ export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeD | |
| } | ||
| export type ContainerType = keyof ContainerTypeDataMapping; | ||
| export type TaskableType = keyof TaskableDocumentMapping; | ||
| export type AssessableType = keyof AssessableDataMapping; | ||
|
|
||
| type KeysWithCode<T> = { | ||
| [K in keyof T]: 'code' extends keyof T[K] ? K : never; | ||
|
|
@@ -160,20 +176,29 @@ export interface ContainerTypeModelMapping { | |
| ['_container_placeholder_']: iDocumentContainer<ContainerType>; // placeholder to avoid empty interface error | ||
| } | ||
|
|
||
| export interface TaskableTypeModelMapping { | ||
| export interface AssessableTypeModelMapping { | ||
| ['true_false_answer']: TrueFalseAnswer; | ||
| ['choice_answer']: ChoiceAnswer; | ||
| ['quiz']: Quiz; | ||
| } | ||
| // enforce all AssessableTypeModels to extend iAssessable: | ||
| type EnsureAllAssessable<T extends { [K in keyof T]: iAssessable<any> }> = T; | ||
| null as unknown as EnsureAllAssessable<AssessableTypeModelMapping>; | ||
|
|
||
| export interface TaskableTypeModelMapping extends AssessableTypeModelMapping { | ||
| ['task_state']: TaskState; | ||
| ['progress_state']: ProgressState; | ||
| } | ||
| // enforce all TaskableTypeMpdels to extend iTaskableDocument: | ||
| type EnsureAllTaskable<T extends { [K in keyof T]: iTaskableDocument<any> }> = T; | ||
| null as unknown as EnsureAllTaskable<TaskableTypeModelMapping>; | ||
|
|
||
| export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTypeModelMapping { | ||
| export interface TypeModelMapping | ||
| extends TaskableTypeModelMapping, ContainerTypeModelMapping, AssessableTypeModelMapping { | ||
| ['code']: Code; | ||
| // TODO: rename to `code_version`? | ||
| ['script_version']: ScriptVersion; | ||
| ['string']: String; | ||
| ['choice_answer']: ChoiceAnswer; | ||
| ['quill_v2']: QuillV2; | ||
| ['solution']: Solution; | ||
| ['dir']: Directory; | ||
|
|
@@ -192,6 +217,7 @@ export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTyp | |
|
|
||
| export type ContainerModelType = ContainerTypeModelMapping[ContainerType]; | ||
| export type TaskableModelType = TaskableTypeModelMapping[TaskableType]; | ||
| export type AssessableModelType = AssessableTypeModelMapping[AssessableType]; | ||
|
|
||
| export type DocumentType = keyof TypeModelMapping; | ||
| export type DocumentModelType = TypeModelMapping[DocumentType]; | ||
|
|
||
83 changes: 83 additions & 0 deletions
83
src/components/documents/Assessable/ChoiceAnswer/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { observer } from 'mobx-react-lite'; | ||
| import React from 'react'; | ||
| import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentType'; | ||
| import Loader from '@tdev-components/Loader'; | ||
| import useIsBrowser from '@docusaurus/useIsBrowser'; | ||
| import { useDocumentRootId } from '@tdev-hooks/useContextDocumentRootId'; | ||
| import { useFirstDocumentBy } from '@tdev-hooks/useFirstDocumentBy'; | ||
| import { DocContext } from '@tdev-components/documents/DocumentContext'; | ||
| import { AssessableComponentProps } from '@tdev-models/documents/Assessable/AssessableMeta'; | ||
| import { type default as ChoiceAnswerModel, ModelMeta } from '@tdev-models/documents/Assessable/ChoiceAnswer'; | ||
| import Options from '../Inputs/Options'; | ||
| import Option, { Props as OptionProps } from '../Inputs/Option'; | ||
| import QuestionCard from '../QuestionCard'; | ||
| import { action } from 'mobx'; | ||
| import { useDocument } from '@tdev-hooks/useContextDocument'; | ||
|
|
||
| interface SharedProps extends AssessableComponentProps<'choice_answer'> { | ||
| multiple?: boolean; | ||
| randomizeOptions?: boolean; | ||
| optionsCount: number; | ||
| } | ||
|
|
||
| export interface StandaloneProps extends SharedProps { | ||
| id: string; | ||
| qid: never; | ||
| } | ||
|
|
||
| export interface InQuizProps extends SharedProps { | ||
| qid: string; | ||
| } | ||
|
|
||
| export type ChoiceAnswerProps = StandaloneProps | InQuizProps; | ||
|
|
||
| interface ThinWrapperProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| type ChoiceAnswerSubComponents = { | ||
| Options: React.FC<ThinWrapperProps>; | ||
| Option: React.FC<OptionProps<'choice_answer'>>; | ||
| }; | ||
|
|
||
| const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { | ||
| const [meta] = React.useState(new ModelMeta(props)); | ||
| const docRootId = useDocumentRootId(props.id); | ||
|
|
||
| const doc = useFirstDocumentBy(docRootId, meta, props.qid); | ||
| const isBrowser = useIsBrowser(); | ||
|
|
||
| if (!doc) { | ||
| return <UnknownDocumentType type={meta.type} />; | ||
| } | ||
|
|
||
| if (!isBrowser) { | ||
| return <Loader />; | ||
| } | ||
|
|
||
| return ( | ||
| <QuestionCard doc={doc}> | ||
| <DocContext.Provider value={doc}>{props.children}</DocContext.Provider> | ||
| </QuestionCard> | ||
| ); | ||
| }) as React.FC<ChoiceAnswerProps> & ChoiceAnswerSubComponents; | ||
|
Comment on lines
+59
to
+63
Contributor
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. After the refactor, this is all that is needed for the Choice Answer :) |
||
|
|
||
| ChoiceAnswer.Options = Options; | ||
|
|
||
| const onUpdateSelection = action((doc: ChoiceAnswerModel, optionIndex: number, checked: boolean) => { | ||
| doc.updateSelection(optionIndex, checked, doc.multiple); | ||
| }); | ||
| ChoiceAnswer.Option = observer((props: Omit<OptionProps<'choice_answer'>, 'onChange'>) => { | ||
| const doc = useDocument<'choice_answer'>(); | ||
| return ( | ||
| <Option | ||
| {...props} | ||
| onChange={onUpdateSelection} | ||
| optionOrder={doc.optionsDisplayOrder(props.optionIndex)} | ||
| isChecked={doc.choices.has(props.optionIndex)} | ||
| multiple={doc.multiple} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| export default ChoiceAnswer; | ||
61 changes: 61 additions & 0 deletions
61
src/components/documents/Assessable/Feedback/QuestionScore.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import Icon from '@mdi/react'; | ||
| import { observer } from 'mobx-react-lite'; | ||
| import styles from './styles.module.scss'; | ||
| import React from 'react'; | ||
| import { QuestionScoringHint } from '../Hints'; | ||
| import useIsMobileView from '@tdev-hooks/useIsMobileView'; | ||
| import { Correctness, CorrectnessColors } from '@tdev-models/documents/Assessable/iAssessable'; | ||
| import { mdiCheckCircleOutline, mdiCloseCircleOutline, mdiProgressCheck, mdiProgressQuestion } from '@mdi/js'; | ||
| import type { AssessableType, AssessableTypeModelMapping } from '@tdev-api/document'; | ||
| import Badge from '@tdev-components/shared/Badge'; | ||
| import { useScrollTo } from '@tdev-hooks/useScrollTo'; | ||
| import clsx from 'clsx'; | ||
|
|
||
| interface FeedbackBadgeProps<T extends AssessableType> { | ||
| doc: AssessableTypeModelMapping[T]; | ||
| } | ||
|
|
||
| const ICONS_BY_CORRECTNESS: Record<Correctness, string> = { | ||
| [Correctness.Correct]: mdiCheckCircleOutline, | ||
| [Correctness.Incorrect]: mdiCloseCircleOutline, | ||
| [Correctness.PartiallyCorrect]: mdiProgressCheck, | ||
| [Correctness.NA]: mdiProgressQuestion | ||
| }; | ||
|
|
||
| export const QuestionScore = observer(<T extends AssessableType>(props: FeedbackBadgeProps<T>) => { | ||
| const { doc } = props; | ||
| const isMobileView = useIsMobileView(); | ||
| const [ref, animate] = useScrollTo(doc); | ||
|
|
||
| if (!doc) { | ||
| return null; | ||
| } | ||
| const maxPoints = doc.assessment?.scoring?.maxPoints ?? doc.maxHits; | ||
| const maxPointsText = isMobileView ? `${maxPoints}p` : `${maxPoints} Punkt${maxPoints !== 1 ? 'e' : ''}`; | ||
|
|
||
| return ( | ||
| <div className={clsx(styles.feedbackBadge, animate && styles.animate)} ref={ref}> | ||
| {doc.assessment?.scoring && ( | ||
| <QuestionScoringHint | ||
| doc={doc} | ||
| trigger={ | ||
| <span> | ||
| <Badge type="secondary" className={styles.pointsBadge}> | ||
| {doc.isAssessed && `${doc.assessment.scoring.pointsAchieved}/`} | ||
| {maxPointsText} | ||
| </Badge> | ||
| </span> | ||
| } | ||
| /> | ||
| )} | ||
| {!doc.assessment?.scoring && <QuestionScoringHint doc={doc} />} | ||
| {doc.isAssessed && ( | ||
| <Icon | ||
| path={ICONS_BY_CORRECTNESS[doc.correctness]} | ||
| color={CorrectnessColors[doc.correctness]} | ||
| size={1} | ||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| }); |
37 changes: 37 additions & 0 deletions
37
src/components/documents/Assessable/Feedback/QuizScore.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { observer } from 'mobx-react-lite'; | ||
| import styles from './styles.module.scss'; | ||
| import React from 'react'; | ||
| import useIsMobileView from '@tdev-hooks/useIsMobileView'; | ||
| import Quiz from '@tdev-models/documents/Assessable/Quiz'; | ||
| import Badge from '@tdev-components/shared/Badge'; | ||
|
|
||
| interface QuizScoreProps { | ||
| doc: Quiz; | ||
| } | ||
|
|
||
| export const QuizScore = observer(({ doc }: QuizScoreProps) => { | ||
| const isMobileView = useIsMobileView(); | ||
|
|
||
| if (!doc || doc.maxHits === 0) { | ||
| // No scoring, so we don't show anything. | ||
| return null; | ||
| } | ||
| const maxPoints = doc.assessment?.scoring?.maxPoints ?? doc.maxHits; | ||
| const maxPointsText = isMobileView ? `${maxPoints}p` : `${maxPoints} Punkt${maxPoints !== 1 ? 'e' : ''}`; | ||
|
|
||
| if (!doc.isAssessed) { | ||
| return ( | ||
| <Badge type="primary" className={styles.pointsBadge}> | ||
| {isMobileView ? `Max.: ${maxPointsText}` : `Zu erreichen: ${maxPointsText}`} | ||
| </Badge> | ||
| ); | ||
| } | ||
| const totalPointsAchieved = doc.assessment?.scoring?.pointsAchieved ?? 'N/A'; | ||
| return ( | ||
| <Badge type="primary" className={styles.pointsBadge}> | ||
| {isMobileView | ||
| ? `${totalPointsAchieved}/${maxPointsText}` | ||
| : `Ergebnis: ${totalPointsAchieved}/${maxPointsText}`} | ||
| </Badge> | ||
| ); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Adds a default value — implementing models, e.g. ChoiceAnswer can use this flag to hide themselves from the Taskable Overview when inside a quiz.