@tinyweb_dev/oe-exam-sdk 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/exams/take/ExamTakingPageContainer.js +6 -0
- package/dist/components/exams/take/components/QuestionRenderer.js +13 -3
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.d.ts +14 -0
- package/dist/components/exams/take/components/question-renderers/ChooseThenAnswerGroupRenderer.js +32 -0
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.d.ts +1 -1
- package/dist/components/exams/take/components/question-renderers/MoversAnswerTheQuestionRenderer.js +10 -2
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.d.ts +16 -0
- package/dist/components/exams/take/components/question-renderers/SortWordsIntoCategoriesRenderer.js +23 -0
- package/dist/components/exams/take/components/question-renderers/index.d.ts +2 -0
- package/dist/components/exams/take/components/question-renderers/index.js +2 -0
- package/dist/components/exams/take/utils/answer-transformers.js +34 -1
- package/dist/components/exams/take/utils/question-transformers.d.ts +42 -3
- package/dist/components/exams/take/utils/question-transformers.js +71 -1
- package/dist/components/questions/_shared/config/question-types.config.js +12 -0
- package/dist/components/questions/_shared/types/answer-the-question-group.type.d.ts +5 -1
- package/dist/components/questions/_shared/types/answer-the-question-group.type.js +7 -0
- package/dist/components/questions/_shared/types/answer-the-question.type.d.ts +3 -1
- package/dist/components/questions/_shared/types/answer-the-question.type.js +19 -1
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.d.ts +47 -0
- package/dist/components/questions/_shared/types/choose-then-answer-group.type.js +2 -0
- package/dist/components/questions/_shared/types/index.d.ts +2 -0
- package/dist/components/questions/_shared/types/index.js +2 -0
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.d.ts +9 -3
- package/dist/components/questions/_shared/types/match-word-to-picture-group.type.js +19 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.d.ts +43 -0
- package/dist/components/questions/_shared/types/sort-words-into-categories.type.js +1 -0
- package/dist/components/questions/components/QuestionSearchDropdown.js +2 -0
- package/dist/components/questions/components/QuestionTypeSelector.js +2 -0
- package/dist/components/questions/creator/question-type-registry.js +4 -0
- package/dist/components/questions/types/answer-the-question/AnswerTheQuestionClient.js +30 -6
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.d.ts +2 -0
- package/dist/components/questions/types/answer-the-question/map-answer-the-question-data.js +56 -0
- package/dist/components/questions/types/answer-the-question/transform.js +9 -3
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupClient.js +42 -9
- package/dist/components/questions/types/answer-the-question-group/AnswerTheQuestionGroupCreator.js +7 -5
- package/dist/components/questions/types/answer-the-question-group/map-answer-the-question-group-data.js +18 -4
- package/dist/components/questions/types/answer-the-question-group/transform.js +8 -2
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupClient.js +34 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/ChooseThenAnswerGroupCreator.js +174 -0
- package/dist/components/questions/types/choose-then-answer-group/index.d.ts +5 -0
- package/dist/components/questions/types/choose-then-answer-group/index.js +5 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.d.ts +11 -0
- package/dist/components/questions/types/choose-then-answer-group/map-choose-then-answer-group-data.js +127 -0
- package/dist/components/questions/types/choose-then-answer-group/register.d.ts +3 -0
- package/dist/components/questions/types/choose-then-answer-group/register.js +37 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.d.ts +2 -0
- package/dist/components/questions/types/choose-then-answer-group/transform.js +37 -0
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupClient.js +39 -10
- package/dist/components/questions/types/match-word-to-picture-group/MatchWordToPictureGroupCreator.js +15 -8
- package/dist/components/questions/types/match-word-to-picture-group/map-match-word-to-picture-group-data.js +8 -3
- package/dist/components/questions/types/match-word-to-picture-group/transform.js +21 -13
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts +12 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js +74 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js +24 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js +171 -0
- package/dist/components/questions/types/sort-words-into-categories/index.d.ts +4 -0
- package/dist/components/questions/types/sort-words-into-categories/index.js +4 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.d.ts +11 -0
- package/dist/components/questions/types/sort-words-into-categories/map-sort-words-into-categories-data.js +153 -0
- package/dist/components/questions/types/sort-words-into-categories/register.d.ts +3 -0
- package/dist/components/questions/types/sort-words-into-categories/register.js +37 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.d.ts +2 -0
- package/dist/components/questions/types/sort-words-into-categories/transform.js +33 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.d.ts +9 -0
- package/dist/components/questions/viewer/AnswerTheQuestionViewer.js +8 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.d.ts +10 -0
- package/dist/components/questions/viewer/ChooseThenAnswerGroupViewer.js +39 -0
- package/dist/components/questions/viewer/QuestionViewer.js +13 -2
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.d.ts +10 -0
- package/dist/components/questions/viewer/SortWordsIntoCategoriesViewer.js +27 -0
- package/dist/components/questions/viewer/index.d.ts +3 -0
- package/dist/components/questions/viewer/index.js +3 -0
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.d.ts +2 -1
- package/dist/components/results/renderers/ReviewAnswerTheQuestionGroupRenderer.js +12 -3
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -1
- package/dist/components/results/renderers/review-question-body-movers.js +24 -2
- package/dist/components/results/review-data.js +28 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/constants/question-skills.js +4 -0
- package/dist/shared/lib/utils/question-reverse-transform.js +98 -2
- package/dist/shared/lib/utils/question-transform-types.d.ts +1 -1
- package/dist/shared/lib/utils/question-transform.js +4 -0
- package/dist/shared/types/common.types.d.ts +1 -1
- package/dist/shared/types/questions/answer-the-question-group.d.ts +3 -0
- package/dist/shared/types/questions/choose-then-answer-group.d.ts +46 -0
- package/dist/shared/types/questions/choose-then-answer-group.js +1 -0
- package/dist/shared/types/questions/index.d.ts +2 -0
- package/dist/shared/types/questions/index.js +3 -0
- package/dist/shared/types/questions/match-word-to-picture-group.d.ts +6 -2
- package/dist/shared/types/questions/sort-words-into-categories.d.ts +37 -0
- package/dist/shared/types/questions/sort-words-into-categories.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, fromMatchWordToPictureImageUrls, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
@@ -30,7 +30,7 @@ export function mapMatchWordToPictureGroupItem(item, index, fallbackAnswer) {
|
|
|
30
30
|
const correctAnswer = asRecord(record.correctAnswer);
|
|
31
31
|
const isExample = record.isExample === true;
|
|
32
32
|
return {
|
|
33
|
-
imageUrl:
|
|
33
|
+
imageUrl: fromMatchWordToPictureImageUrls(content.imageUrl ?? record.imageUrl),
|
|
34
34
|
correctAnswer: asString(correctAnswer.answer) ||
|
|
35
35
|
asString(record.correctAnswer) ||
|
|
36
36
|
asString(fallbackAnswer),
|
|
@@ -58,12 +58,17 @@ export function mapQuestionToMatchWordToPictureGroupData(question) {
|
|
|
58
58
|
? asStringList(answer.wordBank)
|
|
59
59
|
: Array.from(new Set(items.map((item) => item.correctAnswer).filter(Boolean)));
|
|
60
60
|
const title = asString(meta.title) || asString(answer.title);
|
|
61
|
-
const
|
|
61
|
+
const passage = asString(meta.passage).trim() || asString(answer.passage).trim();
|
|
62
|
+
const audioUrl = asString(meta.audioUrl).trim() || asString(answer.audioUrl).trim();
|
|
63
|
+
const imageUrl = fromMatchWordToPictureImageUrls(meta.imageUrl)
|
|
64
|
+
?? fromMatchWordToPictureImageUrls(answer.imageUrl);
|
|
62
65
|
return {
|
|
63
66
|
...(title ? { title } : {}),
|
|
64
67
|
instruction: asString(meta.instruction) ||
|
|
65
68
|
asString(answer.instruction) ||
|
|
66
69
|
MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION,
|
|
70
|
+
...(passage ? { passage } : {}),
|
|
71
|
+
...(audioUrl ? { audioUrl } : {}),
|
|
67
72
|
...(imageUrl ? { imageUrl } : {}),
|
|
68
73
|
wordBank,
|
|
69
74
|
items,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, MATCH_WORD_TO_PICTURE_ITEM_TYPE, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
1
|
+
import { MATCH_WORD_TO_PICTURE_GROUP_DEFAULT_INSTRUCTION, MATCH_WORD_TO_PICTURE_ITEM_TYPE, fromMatchWordToPictureImageUrls, } from '../../_shared/types/match-word-to-picture-group.type';
|
|
2
2
|
function sumGradablePoints(items) {
|
|
3
3
|
return items.reduce((total, item) => {
|
|
4
4
|
if (item.isExample)
|
|
@@ -12,16 +12,20 @@ export const transformMatchWordToPictureGroup = (question) => {
|
|
|
12
12
|
const wordBank = Array.isArray(answerData?.wordBank)
|
|
13
13
|
? answerData.wordBank.filter((word) => typeof word === 'string').map((word) => word.trim()).filter(Boolean)
|
|
14
14
|
: [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
const groupImageUrl = fromMatchWordToPictureImageUrls(answerData?.imageUrl);
|
|
16
|
+
const apiItems = items.map((item, index) => {
|
|
17
|
+
const itemImageUrl = fromMatchWordToPictureImageUrls(item.imageUrl);
|
|
18
|
+
return {
|
|
19
|
+
questionType: MATCH_WORD_TO_PICTURE_ITEM_TYPE,
|
|
20
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
21
|
+
content: {
|
|
22
|
+
...(itemImageUrl ? { imageUrl: itemImageUrl } : {}),
|
|
23
|
+
},
|
|
24
|
+
correctAnswer: { answer: item.correctAnswer || '' },
|
|
25
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
26
|
+
questionNumber: item.questionNumber || index + 1,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
25
29
|
return {
|
|
26
30
|
apiContent: {
|
|
27
31
|
meta: {
|
|
@@ -29,9 +33,13 @@ export const transformMatchWordToPictureGroup = (question) => {
|
|
|
29
33
|
...(typeof answerData?.title === 'string' && answerData.title.trim()
|
|
30
34
|
? { title: answerData.title.trim() }
|
|
31
35
|
: {}),
|
|
32
|
-
...(typeof answerData?.
|
|
33
|
-
? {
|
|
36
|
+
...(typeof answerData?.passage === 'string' && answerData.passage.trim()
|
|
37
|
+
? { passage: answerData.passage }
|
|
38
|
+
: {}),
|
|
39
|
+
...(typeof answerData?.audioUrl === 'string' && answerData.audioUrl.trim()
|
|
40
|
+
? { audioUrl: answerData.audioUrl.trim() }
|
|
34
41
|
: {}),
|
|
42
|
+
...(groupImageUrl ? { imageUrl: groupImageUrl } : {}),
|
|
35
43
|
wordBank,
|
|
36
44
|
},
|
|
37
45
|
items: apiItems,
|
package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SortWordsCategory, SortWordsPlacement, SortWordsWordItem } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
interface SortWordsIntoCategoriesBoardProps {
|
|
3
|
+
categories: SortWordsCategory[];
|
|
4
|
+
words: SortWordsWordItem[];
|
|
5
|
+
placements: SortWordsPlacement[];
|
|
6
|
+
correctPlacements?: SortWordsPlacement[];
|
|
7
|
+
onChange?: (next: SortWordsPlacement[]) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isReviewMode?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function SortWordsIntoCategoriesBoard({ categories, words, placements, correctPlacements, onChange, disabled, isReviewMode, }: SortWordsIntoCategoriesBoardProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesBoard.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { cn } from '../../../../shared/lib/utils';
|
|
5
|
+
const POOL_ZONE = '__pool__';
|
|
6
|
+
function placementMap(items) {
|
|
7
|
+
return new Map(items.map((item) => [item.wordId, item.categoryId]));
|
|
8
|
+
}
|
|
9
|
+
export function SortWordsIntoCategoriesBoard({ categories, words, placements, correctPlacements, onChange, disabled = false, isReviewMode = false, }) {
|
|
10
|
+
const [selectedWordId, setSelectedWordId] = useState(null);
|
|
11
|
+
const current = useMemo(() => placementMap(placements), [placements]);
|
|
12
|
+
const correct = useMemo(() => (correctPlacements ? placementMap(correctPlacements) : new Map()), [correctPlacements]);
|
|
13
|
+
const wordById = useMemo(() => new Map(words.map((word) => [word.id, word])), [words]);
|
|
14
|
+
const emit = (wordId, categoryId) => {
|
|
15
|
+
const word = wordById.get(wordId);
|
|
16
|
+
if (!word || word.isExample || disabled)
|
|
17
|
+
return;
|
|
18
|
+
const next = words.map((item) => ({
|
|
19
|
+
wordId: item.id,
|
|
20
|
+
categoryId: item.id === wordId ? categoryId : (current.get(item.id) ?? item.categoryId ?? ''),
|
|
21
|
+
}));
|
|
22
|
+
onChange?.(next);
|
|
23
|
+
};
|
|
24
|
+
const handleSelect = (wordId) => {
|
|
25
|
+
const word = wordById.get(wordId);
|
|
26
|
+
if (!word || word.isExample || disabled)
|
|
27
|
+
return;
|
|
28
|
+
setSelectedWordId((prev) => (prev === wordId ? null : wordId));
|
|
29
|
+
};
|
|
30
|
+
const handlePlace = (zoneId) => {
|
|
31
|
+
if (!selectedWordId)
|
|
32
|
+
return;
|
|
33
|
+
emit(selectedWordId, zoneId === POOL_ZONE ? '' : zoneId);
|
|
34
|
+
setSelectedWordId(null);
|
|
35
|
+
};
|
|
36
|
+
const handleDrop = (event, zoneId) => {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
const wordId = event.dataTransfer.getData('text/word-id');
|
|
39
|
+
if (!wordId)
|
|
40
|
+
return;
|
|
41
|
+
emit(wordId, zoneId === POOL_ZONE ? '' : zoneId);
|
|
42
|
+
setSelectedWordId(null);
|
|
43
|
+
};
|
|
44
|
+
const poolWords = words.filter((word) => {
|
|
45
|
+
if (word.isExample)
|
|
46
|
+
return false;
|
|
47
|
+
return !(current.get(word.id) || '');
|
|
48
|
+
});
|
|
49
|
+
const renderChip = (word) => {
|
|
50
|
+
const expected = correct.get(word.id) || word.categoryId || '';
|
|
51
|
+
const actual = current.get(word.id) ?? '';
|
|
52
|
+
const isSelected = selectedWordId === word.id;
|
|
53
|
+
const showReview = isReviewMode && !word.isExample && Boolean(expected);
|
|
54
|
+
const isCorrect = showReview && actual === expected;
|
|
55
|
+
const isWrong = showReview && actual !== expected;
|
|
56
|
+
return (_jsxs("button", { type: "button", draggable: !disabled && !word.isExample, onDragStart: (event) => {
|
|
57
|
+
event.dataTransfer.setData('text/word-id', word.id);
|
|
58
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
59
|
+
}, onClick: () => handleSelect(word.id), onDoubleClick: () => {
|
|
60
|
+
if (!word.isExample)
|
|
61
|
+
emit(word.id, '');
|
|
62
|
+
}, disabled: disabled || word.isExample, className: cn('rounded-md border px-3 py-1.5 text-sm shadow-sm transition-colors', word.isExample && 'cursor-default opacity-80', isSelected && 'border-black bg-black text-white', !isSelected && !isCorrect && !isWrong && 'border-gray-300 bg-white text-gray-900', isCorrect && 'border-emerald-500 bg-emerald-50 text-emerald-900', isWrong && 'border-red-500 bg-red-50 text-red-900'), children: [word.text || word.id, word.isExample && (_jsx("span", { className: "ml-1 text-[10px] uppercase tracking-wide opacity-70", children: "ex" }))] }, word.id));
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "min-h-[56px] rounded-lg border border-gray-200 bg-white p-3", onDragOver: (event) => event.preventDefault(), onDrop: (event) => handleDrop(event, POOL_ZONE), onClick: (event) => {
|
|
65
|
+
if (event.target === event.currentTarget)
|
|
66
|
+
handlePlace(POOL_ZONE);
|
|
67
|
+
}, children: _jsx("div", { className: "flex flex-wrap gap-2", children: poolWords.length === 0 ? (_jsx("p", { className: "text-sm text-gray-400", children: disabled ? 'Không còn từ trong pool.' : 'Chọn từ rồi bấm nhóm, hoặc kéo thả. Bấm nền này để trả từ về pool.' })) : (poolWords.map(renderChip)) }) }), _jsx("div", { className: cn('grid gap-4', categories.length <= 2 ? 'md:grid-cols-2' : 'md:grid-cols-3'), children: categories.map((category) => {
|
|
68
|
+
const boxed = words.filter((word) => (current.get(word.id) || '') === category.id);
|
|
69
|
+
return (_jsxs("section", { className: "overflow-hidden rounded-lg border border-gray-300 bg-white", onDragOver: (event) => event.preventDefault(), onDrop: (event) => handleDrop(event, category.id), children: [_jsx("button", { type: "button", className: "w-full bg-gray-200 px-3 py-2 text-center text-sm font-semibold text-gray-800", onClick: () => handlePlace(category.id), disabled: disabled, children: category.name || category.id }), _jsx("div", { className: "flex min-h-[160px] flex-wrap content-start gap-2 p-3", onClick: (event) => {
|
|
70
|
+
if (event.target === event.currentTarget)
|
|
71
|
+
handlePlace(category.id);
|
|
72
|
+
}, children: boxed.map(renderChip) })] }, category.id));
|
|
73
|
+
}) })] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { SortWordsIntoCategoriesClientProps } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
export declare function SortWordsIntoCategoriesClient({ questionData, isReviewMode, userPlacements, onPlacementsChange, }: SortWordsIntoCategoriesClientProps): import("react").JSX.Element;
|
package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesClient.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Lightbulb } from 'lucide-react';
|
|
4
|
+
import { SortWordsIntoCategoriesBoard } from './SortWordsIntoCategoriesBoard';
|
|
5
|
+
import { buildPlacements, padPlacements } from './map-sort-words-into-categories-data';
|
|
6
|
+
export function SortWordsIntoCategoriesClient({ questionData, isReviewMode = false, userPlacements, onPlacementsChange, }) {
|
|
7
|
+
const correctPlacements = buildPlacements(questionData.words);
|
|
8
|
+
const exampleLocked = questionData.words.map((word) => ({
|
|
9
|
+
wordId: word.id,
|
|
10
|
+
categoryId: word.isExample ? word.categoryId : '',
|
|
11
|
+
}));
|
|
12
|
+
const rawPlacements = isReviewMode && !userPlacements
|
|
13
|
+
? correctPlacements
|
|
14
|
+
: (userPlacements ?? exampleLocked);
|
|
15
|
+
const placements = padPlacements(questionData.words, rawPlacements).map((item) => {
|
|
16
|
+
const word = questionData.words.find((entry) => entry.id === item.wordId);
|
|
17
|
+
if (word?.isExample) {
|
|
18
|
+
return { wordId: item.wordId, categoryId: word.categoryId };
|
|
19
|
+
}
|
|
20
|
+
return item;
|
|
21
|
+
});
|
|
22
|
+
const canEdit = Boolean(onPlacementsChange) && !isReviewMode;
|
|
23
|
+
return (_jsxs("div", { className: "space-y-4", children: [(questionData.title || questionData.instruction) && (_jsxs("div", { className: "rounded-xl border border-slate-200 bg-slate-50 px-4 py-3", children: [questionData.title && (_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-slate-500", children: questionData.title })), questionData.instruction && (_jsx("p", { className: "text-sm font-medium text-slate-800", children: questionData.instruction }))] })), _jsx(SortWordsIntoCategoriesBoard, { categories: questionData.categories, words: questionData.words, placements: placements, correctPlacements: isReviewMode ? correctPlacements : undefined, onChange: canEdit ? onPlacementsChange : undefined, disabled: !canEdit, isReviewMode: isReviewMode }), isReviewMode && questionData.explanation && (_jsxs("div", { className: "flex items-start gap-2 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800", children: [_jsx(Lightbulb, { className: "mt-0.5 h-4 w-4 flex-shrink-0" }), questionData.explanation] }))] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { SortWordsIntoCategoriesCreatorProps } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
export declare function SortWordsIntoCategoriesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }: SortWordsIntoCategoriesCreatorProps): import("react").JSX.Element;
|
package/dist/components/questions/types/sort-words-into-categories/SortWordsIntoCategoriesCreator.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../../../../components/ui/button';
|
|
5
|
+
import { Label } from '../../../../components/ui/label';
|
|
6
|
+
import { Input } from '../../../../components/ui/input';
|
|
7
|
+
import { PointsInput } from '../../../../components/ui/points-input';
|
|
8
|
+
import { Textarea } from '../../../../components/ui/textarea';
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../../components/ui/card';
|
|
10
|
+
import { Switch } from '../../../../components/ui/switch';
|
|
11
|
+
import { useDebouncedCallback } from '../../../../shared/lib/hooks';
|
|
12
|
+
import { Eye, Pencil, Plus, Star, Trash2 } from 'lucide-react';
|
|
13
|
+
import { SortWordsIntoCategoriesClient } from './SortWordsIntoCategoriesClient';
|
|
14
|
+
import { createEmptyCategory, createEmptyWord, sumGradablePoints, } from './map-sort-words-into-categories-data';
|
|
15
|
+
import { SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION } from '../../_shared/types/sort-words-into-categories.type';
|
|
16
|
+
function normalizeCategories(raw) {
|
|
17
|
+
if (!Array.isArray(raw) || raw.length < 2) {
|
|
18
|
+
const first = createEmptyCategory([]);
|
|
19
|
+
const second = createEmptyCategory([first.id]);
|
|
20
|
+
return [
|
|
21
|
+
{ ...first, name: raw?.[0]?.name || 'Technology item' },
|
|
22
|
+
{ ...second, name: raw?.[1]?.name || 'Activity using technology' },
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
return raw.map((item, index) => ({
|
|
26
|
+
id: item.id || `cat_${index + 1}`,
|
|
27
|
+
name: item.name || '',
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function normalizeWords(raw, categories) {
|
|
31
|
+
const fallbackCategory = categories[0]?.id || '';
|
|
32
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
33
|
+
const first = createEmptyWord([], fallbackCategory);
|
|
34
|
+
const second = createEmptyWord([first.id], categories[1]?.id || fallbackCategory);
|
|
35
|
+
return [first, second];
|
|
36
|
+
}
|
|
37
|
+
return raw.map((item, index) => ({
|
|
38
|
+
id: item.id || `w${index + 1}`,
|
|
39
|
+
text: item.text || '',
|
|
40
|
+
categoryId: item.categoryId || fallbackCategory,
|
|
41
|
+
isExample: item.isExample === true,
|
|
42
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
export function SortWordsIntoCategoriesCreator({ initialData, onChange, externalErrors, onUnsavedChangesChange, validationRef, }) {
|
|
46
|
+
const [title, setTitle] = useState(initialData?.title || '');
|
|
47
|
+
const [instruction, setInstruction] = useState(initialData?.instruction || SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION);
|
|
48
|
+
const [categories, setCategories] = useState(() => normalizeCategories(initialData?.categories));
|
|
49
|
+
const [words, setWords] = useState(() => normalizeWords(initialData?.words, normalizeCategories(initialData?.categories)));
|
|
50
|
+
const [explanation, setExplanation] = useState(initialData?.explanation || '');
|
|
51
|
+
const [isClientMode, setIsClientMode] = useState(false);
|
|
52
|
+
const [errors, setErrors] = useState([]);
|
|
53
|
+
const onChangeRef = useRef(onChange);
|
|
54
|
+
const previousPayloadRef = useRef('');
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
onChangeRef.current = onChange;
|
|
57
|
+
}, [onChange]);
|
|
58
|
+
const buildPayload = (nextTitle = title, nextInstruction = instruction, nextCategories = categories, nextWords = words, nextExplanation = explanation) => ({
|
|
59
|
+
title: nextTitle,
|
|
60
|
+
instruction: nextInstruction,
|
|
61
|
+
categories: nextCategories,
|
|
62
|
+
words: nextWords,
|
|
63
|
+
explanation: nextExplanation,
|
|
64
|
+
points: sumGradablePoints(nextWords),
|
|
65
|
+
});
|
|
66
|
+
const debouncedOnChange = useDebouncedCallback((payload) => {
|
|
67
|
+
onChangeRef.current?.(payload);
|
|
68
|
+
}, 300);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const payload = buildPayload();
|
|
71
|
+
const serialized = JSON.stringify(payload);
|
|
72
|
+
if (serialized !== previousPayloadRef.current) {
|
|
73
|
+
previousPayloadRef.current = serialized;
|
|
74
|
+
debouncedOnChange(payload);
|
|
75
|
+
onUnsavedChangesChange?.(true);
|
|
76
|
+
}
|
|
77
|
+
}, [title, instruction, categories, words, explanation, debouncedOnChange, onUnsavedChangesChange]);
|
|
78
|
+
const validate = () => {
|
|
79
|
+
const nextErrors = [];
|
|
80
|
+
if (!instruction.trim()) {
|
|
81
|
+
nextErrors.push('Hướng dẫn (Instruction) không được để trống.');
|
|
82
|
+
}
|
|
83
|
+
if (categories.length < 2) {
|
|
84
|
+
nextErrors.push('Cần ít nhất 2 nhóm.');
|
|
85
|
+
}
|
|
86
|
+
const categoryIds = new Set();
|
|
87
|
+
categories.forEach((category, index) => {
|
|
88
|
+
if (!category.id.trim())
|
|
89
|
+
nextErrors.push(`Nhóm ${index + 1}: id không được để trống.`);
|
|
90
|
+
if (!category.name.trim())
|
|
91
|
+
nextErrors.push(`Nhóm ${index + 1}: tên không được để trống.`);
|
|
92
|
+
if (category.id.trim() && categoryIds.has(category.id.trim())) {
|
|
93
|
+
nextErrors.push(`Nhóm ${index + 1}: id "${category.id}" bị trùng.`);
|
|
94
|
+
}
|
|
95
|
+
categoryIds.add(category.id.trim());
|
|
96
|
+
});
|
|
97
|
+
if (words.length < 1) {
|
|
98
|
+
nextErrors.push('Cần ít nhất 1 từ.');
|
|
99
|
+
}
|
|
100
|
+
const wordIds = new Set();
|
|
101
|
+
words.forEach((word, index) => {
|
|
102
|
+
if (!word.id.trim())
|
|
103
|
+
nextErrors.push(`Từ ${index + 1}: id không được để trống.`);
|
|
104
|
+
if (!word.text.trim())
|
|
105
|
+
nextErrors.push(`Từ ${index + 1}: nội dung không được để trống.`);
|
|
106
|
+
if (word.id.trim() && wordIds.has(word.id.trim())) {
|
|
107
|
+
nextErrors.push(`Từ ${index + 1}: id "${word.id}" bị trùng.`);
|
|
108
|
+
}
|
|
109
|
+
wordIds.add(word.id.trim());
|
|
110
|
+
if (!word.categoryId.trim()) {
|
|
111
|
+
nextErrors.push(`Từ ${index + 1}: chưa chọn nhóm đúng.`);
|
|
112
|
+
}
|
|
113
|
+
else if (!categoryIds.has(word.categoryId)) {
|
|
114
|
+
nextErrors.push(`Từ ${index + 1}: nhóm không hợp lệ.`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
setErrors(nextErrors);
|
|
118
|
+
return nextErrors.length === 0;
|
|
119
|
+
};
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!validationRef)
|
|
122
|
+
return;
|
|
123
|
+
validationRef.current = {
|
|
124
|
+
triggerValidation: async () => validate(),
|
|
125
|
+
getFormData: () => buildPayload(),
|
|
126
|
+
};
|
|
127
|
+
return () => {
|
|
128
|
+
validationRef.current = null;
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
const updateCategory = (index, patch) => {
|
|
132
|
+
setCategories((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
|
|
133
|
+
};
|
|
134
|
+
const updateWord = (index, patch) => {
|
|
135
|
+
setWords((prev) => prev.map((item, i) => {
|
|
136
|
+
if (i !== index)
|
|
137
|
+
return item;
|
|
138
|
+
const next = { ...item, ...patch };
|
|
139
|
+
if (next.isExample)
|
|
140
|
+
next.points = 0;
|
|
141
|
+
return next;
|
|
142
|
+
}));
|
|
143
|
+
};
|
|
144
|
+
const addCategory = () => {
|
|
145
|
+
setCategories((prev) => [...prev, createEmptyCategory(prev.map((item) => item.id))]);
|
|
146
|
+
};
|
|
147
|
+
const removeCategory = (index) => {
|
|
148
|
+
if (categories.length <= 2)
|
|
149
|
+
return;
|
|
150
|
+
const removedId = categories[index]?.id;
|
|
151
|
+
const nextCategories = categories.filter((_, i) => i !== index);
|
|
152
|
+
const fallback = nextCategories[0]?.id || '';
|
|
153
|
+
setCategories(nextCategories);
|
|
154
|
+
setWords((prev) => prev.map((word) => (word.categoryId === removedId ? { ...word, categoryId: fallback } : word)));
|
|
155
|
+
};
|
|
156
|
+
const addWord = () => {
|
|
157
|
+
setWords((prev) => [
|
|
158
|
+
...prev,
|
|
159
|
+
createEmptyWord(prev.map((item) => item.id), categories[0]?.id || ''),
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
const removeWord = (index) => {
|
|
163
|
+
if (words.length <= 1)
|
|
164
|
+
return;
|
|
165
|
+
setWords((prev) => prev.filter((_, i) => i !== index));
|
|
166
|
+
};
|
|
167
|
+
const displayErrors = [...errors, ...(externalErrors || [])];
|
|
168
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setIsClientMode((v) => !v), children: [isClientMode ? _jsx(Pencil, { className: "mr-2 h-4 w-4" }) : _jsx(Eye, { className: "mr-2 h-4 w-4" }), isClientMode ? 'Sửa' : 'Xem trước'] }) }), isClientMode ? (_jsx(SortWordsIntoCategoriesClient, { questionData: buildPayload(), isReviewMode: true })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { children: [_jsx(Label, { htmlFor: "swc-title", children: "Ti\u00EAu \u0111\u1EC1 (optional)" }), _jsx(Input, { id: "swc-title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "VOCABULARY" })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "swc-instruction", children: "H\u01B0\u1EDBng d\u1EABn" }), _jsx(Input, { id: "swc-instruction", value: instruction, onChange: (event) => setInstruction(event.target.value), placeholder: SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsx(CardTitle, { className: "text-base", children: "Nh\u00F3m (categories)" }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addCategory, children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Th\u00EAm nh\u00F3m"] })] }), _jsx(CardContent, { className: "space-y-3", children: categories.map((category, index) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { value: category.name, onChange: (event) => updateCategory(index, { name: event.target.value }), placeholder: `Nhóm ${index + 1}` }), categories.length > 2 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeCategory(index), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }, category.id))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [_jsx(CardTitle, { className: "text-base", children: "T\u1EEB / c\u1EE5m t\u1EEB" }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addWord, children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), " Th\u00EAm t\u1EEB"] })] }), _jsx(CardContent, { className: "space-y-3", children: words.map((word, index) => (_jsxs("div", { className: "grid gap-2 rounded-lg border p-3 md:grid-cols-[1fr_180px_auto]", children: [_jsx(Input, { value: word.text, onChange: (event) => updateWord(index, { text: event.target.value }), placeholder: `Từ #${index + 1} (VD: press a button)` }), _jsx("select", { className: "h-10 rounded-md border border-input bg-background px-3 text-sm", value: word.categoryId, onChange: (event) => updateWord(index, { categoryId: event.target.value }), children: categories.map((category) => (_jsx("option", { value: category.id, children: category.name || category.id }, category.id))) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Switch, { id: `swc-example-${index}`, checked: word.isExample === true, onCheckedChange: (checked) => updateWord(index, { isExample: checked }) }), _jsxs(Label, { htmlFor: `swc-example-${index}`, className: "flex cursor-pointer items-center gap-1 text-xs", children: [_jsx(Star, { className: "h-3 w-3 text-amber-500" }), "Example"] })] }), !word.isExample && (_jsx(PointsInput, { value: word.points, allowZero: true, onChange: (event) => updateWord(index, {
|
|
169
|
+
points: Number.parseInt(event.target.value, 10) || 1,
|
|
170
|
+
}) })), words.length > 1 && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => removeWord(index), children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] })] }, word.id))) })] }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "swc-explanation", children: "Gi\u1EA3i th\u00EDch" }), _jsx(Textarea, { id: "swc-explanation", value: explanation, onChange: (event) => setExplanation(event.target.value) })] })] })), displayErrors.length > 0 && (_jsx("ul", { className: "list-disc space-y-1 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", children: displayErrors.map((error) => (_jsx("li", { children: error }, error))) }))] }));
|
|
171
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SortWordsCategory, SortWordsIntoCategoriesData, SortWordsPlacement, SortWordsWordItem } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
export declare function nextPrefixedId(prefix: string, existing: string[]): string;
|
|
3
|
+
export declare function sumGradablePoints(words: SortWordsWordItem[]): number;
|
|
4
|
+
export declare function buildPlacements(words: SortWordsWordItem[]): SortWordsPlacement[];
|
|
5
|
+
export declare function readPlacements(value: unknown): SortWordsPlacement[] | undefined;
|
|
6
|
+
export declare function padPlacements(words: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
}>, raw: unknown): SortWordsPlacement[];
|
|
9
|
+
export declare function createEmptyCategory(existingIds: string[]): SortWordsCategory;
|
|
10
|
+
export declare function createEmptyWord(existingIds: string[], categoryId: string): SortWordsWordItem;
|
|
11
|
+
export declare function mapQuestionToSortWordsIntoCategoriesData(question: Record<string, unknown> | null | undefined): SortWordsIntoCategoriesData;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return isRecord(value) ? value : {};
|
|
7
|
+
}
|
|
8
|
+
function asString(value, fallback = '') {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
export function nextPrefixedId(prefix, existing) {
|
|
12
|
+
let n = existing.length + 1;
|
|
13
|
+
const used = new Set(existing);
|
|
14
|
+
while (used.has(`${prefix}${n}`))
|
|
15
|
+
n += 1;
|
|
16
|
+
return `${prefix}${n}`;
|
|
17
|
+
}
|
|
18
|
+
export function sumGradablePoints(words) {
|
|
19
|
+
return words.reduce((total, word) => {
|
|
20
|
+
if (word.isExample)
|
|
21
|
+
return total;
|
|
22
|
+
return total + (typeof word.points === 'number' ? word.points : 1);
|
|
23
|
+
}, 0);
|
|
24
|
+
}
|
|
25
|
+
export function buildPlacements(words) {
|
|
26
|
+
return words.map((word) => ({
|
|
27
|
+
wordId: word.id,
|
|
28
|
+
categoryId: word.categoryId || '',
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
export function readPlacements(value) {
|
|
32
|
+
const source = Array.isArray(value)
|
|
33
|
+
? value
|
|
34
|
+
: isRecord(value) && Array.isArray(value.placements)
|
|
35
|
+
? value.placements
|
|
36
|
+
: null;
|
|
37
|
+
if (!source)
|
|
38
|
+
return undefined;
|
|
39
|
+
return source.map((item) => {
|
|
40
|
+
const record = asRecord(item);
|
|
41
|
+
return {
|
|
42
|
+
wordId: asString(record.wordId),
|
|
43
|
+
categoryId: asString(record.categoryId),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function padPlacements(words, raw) {
|
|
48
|
+
const source = readPlacements(raw) ?? [];
|
|
49
|
+
const byWord = new Map();
|
|
50
|
+
source.forEach((item) => {
|
|
51
|
+
const record = asRecord(item);
|
|
52
|
+
const wordId = asString(record.wordId);
|
|
53
|
+
if (!wordId || byWord.has(wordId))
|
|
54
|
+
return;
|
|
55
|
+
byWord.set(wordId, asString(record.categoryId));
|
|
56
|
+
});
|
|
57
|
+
return words.map((word) => ({
|
|
58
|
+
wordId: word.id,
|
|
59
|
+
categoryId: byWord.get(word.id) ?? '',
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
export function createEmptyCategory(existingIds) {
|
|
63
|
+
return {
|
|
64
|
+
id: nextPrefixedId('cat_', existingIds),
|
|
65
|
+
name: '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function createEmptyWord(existingIds, categoryId) {
|
|
69
|
+
return {
|
|
70
|
+
id: nextPrefixedId('w', existingIds),
|
|
71
|
+
text: '',
|
|
72
|
+
categoryId,
|
|
73
|
+
isExample: false,
|
|
74
|
+
points: 1,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function mapCategories(raw) {
|
|
78
|
+
if (!Array.isArray(raw))
|
|
79
|
+
return [];
|
|
80
|
+
return raw.map((item, index) => {
|
|
81
|
+
const record = asRecord(item);
|
|
82
|
+
return {
|
|
83
|
+
id: asString(record.id, `cat_${index + 1}`),
|
|
84
|
+
name: asString(record.name),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function mapWords(rawWords, placements) {
|
|
89
|
+
if (!Array.isArray(rawWords))
|
|
90
|
+
return [];
|
|
91
|
+
const placementMap = new Map(placements.map((item) => [item.wordId, item.categoryId]));
|
|
92
|
+
return rawWords.map((item, index) => {
|
|
93
|
+
const record = asRecord(item);
|
|
94
|
+
const id = asString(record.id, `w${index + 1}`);
|
|
95
|
+
const isExample = record.isExample === true;
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
text: asString(record.text),
|
|
99
|
+
categoryId: asString(record.categoryId) || placementMap.get(id) || '',
|
|
100
|
+
isExample,
|
|
101
|
+
points: isExample ? 0 : (typeof record.points === 'number' ? record.points : 1),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function mapQuestionToSortWordsIntoCategoriesData(question) {
|
|
106
|
+
const source = asRecord(question);
|
|
107
|
+
const content = asRecord(source.content);
|
|
108
|
+
const answer = asRecord(source.answer);
|
|
109
|
+
const correctAnswer = asRecord(source.correctAnswer ?? source.correct_answer ?? answer.correctAnswer);
|
|
110
|
+
const explanation = asString(source.explanation) || asString(answer.explanation);
|
|
111
|
+
const meta = asRecord(content.meta ?? answer.meta);
|
|
112
|
+
const placements = padPlacements(Array.isArray(content.words)
|
|
113
|
+
? content.words.map((item, index) => ({ id: asString(asRecord(item).id, `w${index + 1}`) }))
|
|
114
|
+
: Array.isArray(answer.words)
|
|
115
|
+
? answer.words.map((item, index) => ({ id: asString(asRecord(item).id, `w${index + 1}`) }))
|
|
116
|
+
: [], correctAnswer.placements ?? answer.placements);
|
|
117
|
+
if (Array.isArray(content.words) || Array.isArray(content.categories)) {
|
|
118
|
+
const words = mapWords(content.words, placements);
|
|
119
|
+
return {
|
|
120
|
+
title: asString(meta.title) || asString(answer.title),
|
|
121
|
+
instruction: asString(meta.instruction) ||
|
|
122
|
+
asString(answer.instruction) ||
|
|
123
|
+
SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
|
|
124
|
+
categories: mapCategories(content.categories),
|
|
125
|
+
words,
|
|
126
|
+
explanation,
|
|
127
|
+
points: sumGradablePoints(words),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(answer.words) || Array.isArray(answer.categories)) {
|
|
131
|
+
const words = mapWords(answer.words, placements);
|
|
132
|
+
return {
|
|
133
|
+
title: asString(answer.title),
|
|
134
|
+
instruction: asString(answer.instruction) || SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
|
|
135
|
+
categories: mapCategories(answer.categories),
|
|
136
|
+
words,
|
|
137
|
+
explanation,
|
|
138
|
+
points: typeof answer.points === 'number' ? answer.points : sumGradablePoints(words),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const firstCategory = createEmptyCategory([]);
|
|
142
|
+
const secondCategory = createEmptyCategory([firstCategory.id]);
|
|
143
|
+
const firstWord = createEmptyWord([], firstCategory.id);
|
|
144
|
+
const secondWord = createEmptyWord([firstWord.id], secondCategory.id);
|
|
145
|
+
return {
|
|
146
|
+
title: '',
|
|
147
|
+
instruction: SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
|
|
148
|
+
categories: [firstCategory, secondCategory],
|
|
149
|
+
words: [firstWord, secondWord],
|
|
150
|
+
explanation,
|
|
151
|
+
points: 2,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QuestionTypeRegistration } from '../../creator/question-type-registry';
|
|
2
|
+
export { buildPlacements, createEmptyCategory, createEmptyWord, mapQuestionToSortWordsIntoCategoriesData, padPlacements, readPlacements, sumGradablePoints, } from './map-sort-words-into-categories-data';
|
|
3
|
+
export declare const sortWordsIntoCategoriesRegistration: QuestionTypeRegistration;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SortWordsIntoCategoriesCreator } from './SortWordsIntoCategoriesCreator';
|
|
2
|
+
import { mapQuestionToSortWordsIntoCategoriesData } from './map-sort-words-into-categories-data';
|
|
3
|
+
export { buildPlacements, createEmptyCategory, createEmptyWord, mapQuestionToSortWordsIntoCategoriesData, padPlacements, readPlacements, sumGradablePoints, } from './map-sort-words-into-categories-data';
|
|
4
|
+
export const sortWordsIntoCategoriesRegistration = {
|
|
5
|
+
component: SortWordsIntoCategoriesCreator,
|
|
6
|
+
transformInitialData: (initialData) => mapQuestionToSortWordsIntoCategoriesData(initialData),
|
|
7
|
+
getExtraProps: (ctx) => ({
|
|
8
|
+
onUnsavedChangesChange: ctx.onUnsavedChangesChange,
|
|
9
|
+
validationRef: ctx.validationRef,
|
|
10
|
+
}),
|
|
11
|
+
wrapOnSave: (data, ctx) => {
|
|
12
|
+
const payload = data;
|
|
13
|
+
return {
|
|
14
|
+
type: ctx.questionType,
|
|
15
|
+
content: ctx.state.content,
|
|
16
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
17
|
+
level: ctx.state.level,
|
|
18
|
+
difficulty: ctx.state.difficulty,
|
|
19
|
+
skill: ctx.state.skill,
|
|
20
|
+
answer: payload,
|
|
21
|
+
explanation: payload.explanation || '',
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
wrapOnChange: (data, ctx) => {
|
|
25
|
+
const payload = data;
|
|
26
|
+
return {
|
|
27
|
+
type: ctx.questionType,
|
|
28
|
+
content: ctx.state.content,
|
|
29
|
+
points: payload.points ?? ctx.state.points ?? 1,
|
|
30
|
+
level: ctx.state.level,
|
|
31
|
+
difficulty: ctx.state.difficulty,
|
|
32
|
+
skill: ctx.state.skill,
|
|
33
|
+
answer: payload,
|
|
34
|
+
explanation: payload.explanation || '',
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION } from '../../_shared/types/sort-words-into-categories.type';
|
|
2
|
+
import { sumGradablePoints } from './map-sort-words-into-categories-data';
|
|
3
|
+
export const transformSortWordsIntoCategories = (question) => {
|
|
4
|
+
const answerData = question.answer;
|
|
5
|
+
const categories = Array.isArray(answerData?.categories) ? answerData.categories : [];
|
|
6
|
+
const words = Array.isArray(answerData?.words) ? answerData.words : [];
|
|
7
|
+
const title = typeof answerData?.title === 'string' ? answerData.title.trim() : '';
|
|
8
|
+
return {
|
|
9
|
+
apiContent: {
|
|
10
|
+
meta: {
|
|
11
|
+
instruction: answerData?.instruction || SORT_WORDS_INTO_CATEGORIES_DEFAULT_INSTRUCTION,
|
|
12
|
+
...(title ? { title } : {}),
|
|
13
|
+
},
|
|
14
|
+
categories: categories.map((item) => ({
|
|
15
|
+
id: item.id || '',
|
|
16
|
+
name: item.name || '',
|
|
17
|
+
})),
|
|
18
|
+
words: words.map((item) => ({
|
|
19
|
+
id: item.id || '',
|
|
20
|
+
text: item.text || '',
|
|
21
|
+
points: item.isExample ? 0 : (typeof item.points === 'number' ? item.points : 1),
|
|
22
|
+
...(item.isExample ? { isExample: true } : {}),
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
apiCorrectAnswer: {
|
|
26
|
+
placements: words.map((item) => ({
|
|
27
|
+
wordId: item.id || '',
|
|
28
|
+
categoryId: item.categoryId || '',
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
totalPoints: sumGradablePoints(words),
|
|
32
|
+
};
|
|
33
|
+
};
|