From 9b35a8bf303fe355ccbaf5fb1ab41338a3fe54c9 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 13 May 2026 04:38:09 -0700 Subject: [PATCH] qustion type builder integartion fix --- src/api/questionTypeDefinitions.api.ts | 16 +- .../QuestionTypeConfigStep.tsx | 189 +++++++++++++++--- .../lib/questionTypeDefinitionValidation.ts | 50 +++-- 3 files changed, 209 insertions(+), 46 deletions(-) diff --git a/src/api/questionTypeDefinitions.api.ts b/src/api/questionTypeDefinitions.api.ts index f1970a6..1ac88db 100644 --- a/src/api/questionTypeDefinitions.api.ts +++ b/src/api/questionTypeDefinitions.api.ts @@ -285,10 +285,20 @@ export async function getQuestionTypeDefinitions(params?: { return parseDefinitionsList(raw) } -export async function getQuestionTypeDefinitionById(id: number) { +/** + * GET /questions/type-definitions/:id + * + * Typical success body (axios `res.data`): envelope with nested `data` or `Data` holding the definition. + * Definition fields are often PascalCase (`ID`, `Key`, `DisplayName`, `StimulusComponentKinds`, `StimulusSchema`, + * `ResponseSchema`, `IsSystem`, `Status`, `CreatedAt`, `UpdatedAt`). Envelope `success` may be false; parsing + * does not rely on it. + */ +export async function getQuestionTypeDefinitionById(id: number): Promise { const res = await http.get>(`/questions/type-definitions/${id}`) - const def = unwrapApiPayload(res) - return normalizeTypeDefinitionFromApi(def) ?? undefined + const fromEnvelope = unwrapApiPayload(res) + return ( + normalizeTypeDefinitionFromApi(fromEnvelope) ?? normalizeTypeDefinitionFromApi(res.data) ?? undefined + ) } export async function updateQuestionTypeDefinition( diff --git a/src/pages/content-management/components/question-type-steps/QuestionTypeConfigStep.tsx b/src/pages/content-management/components/question-type-steps/QuestionTypeConfigStep.tsx index a3c6791..5695276 100644 --- a/src/pages/content-management/components/question-type-steps/QuestionTypeConfigStep.tsx +++ b/src/pages/content-management/components/question-type-steps/QuestionTypeConfigStep.tsx @@ -5,11 +5,15 @@ import { ChevronDown, ChevronUp, Hourglass, + Plus, } from "lucide-react" import { Button } from "../../../../components/ui/button" import { Card } from "../../../../components/ui/card" import { Input } from "../../../../components/ui/input" -import type { QuestionTypeDefinitionCreatePayload } from "../../../../types/questionTypeDefinition.types" +import type { + DynamicElementDefinition, + QuestionTypeDefinitionCreatePayload, +} from "../../../../types/questionTypeDefinition.types" import type { FieldErrorMap } from "../../lib/questionTypeDefinitionValidation" import { SchemaBuilderSection } from "./SchemaBuilderSection" import { ComponentKindCard } from "./ComponentKindCard" @@ -33,6 +37,27 @@ function toggleKind(list: string[], kind: string): string[] { return list.includes(kind) ? list.filter((k) => k !== kind) : [...list, kind] } +function slugFragmentFromKind(kind: string): string { + const s = (kind || "field").toLowerCase().replace(/[^a-z0-9]+/g, "_") + return s.replace(/^_|_$/g, "") || "field" +} + +function defaultSchemaLabel(kind: string): string { + return kind.replace(/_/g, " ") +} + +function nextUniqueSchemaElementId(rows: DynamicElementDefinition[], kind: string): string { + const base = slugFragmentFromKind(kind) + const existing = new Set(rows.map((r) => r.id.trim()).filter(Boolean)) + let n = 1 + let id = `${base}_${n}` + while (existing.has(id)) { + n++ + id = `${base}_${n}` + } + return id +} + function rowErrorMap(side: "stimulus" | "response", errors: FieldErrorMap): Record { const prefix = `${side}_` const out: Record = {} @@ -63,6 +88,82 @@ export function QuestionTypeConfigStep({ const title = draft.display_name?.trim() || "Untitled definition" + const handleStimulusKindClick = (kind: string) => { + setDraft((d) => { + const wasSelected = d.stimulus_component_kinds.includes(kind) + const stimulus_component_kinds = toggleKind(d.stimulus_component_kinds, kind) + if (!wasSelected) { + const stimulus_schema = [...d.stimulus_schema] + if (!stimulus_schema.some((r) => r.kind === kind)) { + stimulus_schema.push({ + id: nextUniqueSchemaElementId(stimulus_schema, kind), + kind, + label: defaultSchemaLabel(kind), + required: true, + }) + } + return { ...d, stimulus_component_kinds, stimulus_schema } + } + return { + ...d, + stimulus_component_kinds, + stimulus_schema: d.stimulus_schema.filter((r) => r.kind !== kind), + } + }) + } + + const handleResponseKindClick = (kind: string) => { + setDraft((d) => { + const wasSelected = d.response_component_kinds.includes(kind) + const response_component_kinds = toggleKind(d.response_component_kinds, kind) + if (!wasSelected) { + const response_schema = [...d.response_schema] + if (!response_schema.some((r) => r.kind === kind)) { + response_schema.push({ + id: nextUniqueSchemaElementId(response_schema, kind), + kind, + label: defaultSchemaLabel(kind), + required: true, + }) + } + return { ...d, response_component_kinds, response_schema } + } + return { + ...d, + response_component_kinds, + response_schema: d.response_schema.filter((r) => r.kind !== kind), + } + }) + } + + const addStimulusSlot = (kind: string) => { + setDraft((d) => { + if (!d.stimulus_component_kinds.includes(kind)) return d + const stimulus_schema = [...d.stimulus_schema] + stimulus_schema.push({ + id: nextUniqueSchemaElementId(stimulus_schema, kind), + kind, + label: defaultSchemaLabel(kind), + required: true, + }) + return { ...d, stimulus_schema } + }) + } + + const addResponseSlot = (kind: string) => { + setDraft((d) => { + if (!d.response_component_kinds.includes(kind)) return d + const response_schema = [...d.response_schema] + response_schema.push({ + id: nextUniqueSchemaElementId(response_schema, kind), + kind, + label: defaultSchemaLabel(kind), + required: true, + }) + return { ...d, response_schema } + }) + } + return (
@@ -116,26 +217,43 @@ export function QuestionTypeConfigStep({ Section A: Question input types

- Choose how the question is presented to the learner. + Choose how the question is presented to the learner. The API lists each kind once in{" "} + stimulus_component_kinds{" "} + while stimulus_schema can + include the same kind multiple times (different ids).

{stimulusCatalogKinds.map((kind) => { const { label, Icon } = getStimulusKindPresentation(kind) const selected = draft.stimulus_component_kinds.includes(kind) + const slotCount = draft.stimulus_schema.filter((r) => r.kind === kind).length return ( - - setDraft((d) => ({ - ...d, - stimulus_component_kinds: toggleKind(d.stimulus_component_kinds, kind), - })) - } - /> +
+ handleStimulusKindClick(kind)} + /> + {selected ? ( +
+ + {slotCount} slot{slotCount === 1 ? "" : "s"} + + +
+ ) : null} +
) })}
@@ -150,26 +268,43 @@ export function QuestionTypeConfigStep({ Section B: Answer types

- How should the student answer this question? + How should the student answer?{" "} + response_component_kinds is + deduplicated; use Add slot for multiple + fields of the same kind.

{responseCatalogKinds.map((kind) => { const { label, Icon } = getResponseKindPresentation(kind) const selected = draft.response_component_kinds.includes(kind) + const slotCount = draft.response_schema.filter((r) => r.kind === kind).length return ( - - setDraft((d) => ({ - ...d, - response_component_kinds: toggleKind(d.response_component_kinds, kind), - })) - } - /> +
+ handleResponseKindClick(kind)} + /> + {selected ? ( +
+ + {slotCount} slot{slotCount === 1 ? "" : "s"} + + +
+ ) : null} +
) })}
diff --git a/src/pages/content-management/lib/questionTypeDefinitionValidation.ts b/src/pages/content-management/lib/questionTypeDefinitionValidation.ts index 9ad07e7..f71146f 100644 --- a/src/pages/content-management/lib/questionTypeDefinitionValidation.ts +++ b/src/pages/content-management/lib/questionTypeDefinitionValidation.ts @@ -116,29 +116,47 @@ export function validateDefinitionBasic(payload: { return errors } +/** Unique kinds used in schema rows (API expects deduplicated lists; schema may repeat the same kind). */ +function uniqueKindsFromSchemaRows(rows: DynamicElementDefinition[]): string[] { + const set = new Set() + for (const r of rows) { + const k = r.kind?.trim() + if (k) set.add(k) + } + return [...set].sort((a, b) => a.localeCompare(b)) +} + export function buildCreatePayload( draft: QuestionTypeDefinitionCreatePayload, ): QuestionTypeDefinitionCreatePayload { + const stimulus_schema = draft.stimulus_schema.map((r) => ({ + ...r, + id: r.id.trim(), + kind: r.kind.trim(), + label: r.label?.trim() || undefined, + config: r.config && Object.keys(r.config).length ? r.config : undefined, + })) + const response_schema = draft.response_schema.map((r) => ({ + ...r, + id: r.id.trim(), + kind: r.kind.trim(), + label: r.label?.trim() || undefined, + config: r.config && Object.keys(r.config).length ? r.config : undefined, + })) + + const stimulusKindsFromSchema = uniqueKindsFromSchemaRows(stimulus_schema) + const responseKindsFromSchema = uniqueKindsFromSchemaRows(response_schema) + return { ...draft, key: draft.key.trim(), display_name: draft.display_name.trim(), description: draft.description?.trim() || null, - stimulus_component_kinds: [...draft.stimulus_component_kinds], - response_component_kinds: [...draft.response_component_kinds], - stimulus_schema: draft.stimulus_schema.map((r) => ({ - ...r, - id: r.id.trim(), - kind: r.kind.trim(), - label: r.label?.trim() || undefined, - config: r.config && Object.keys(r.config).length ? r.config : undefined, - })), - response_schema: draft.response_schema.map((r) => ({ - ...r, - id: r.id.trim(), - kind: r.kind.trim(), - label: r.label?.trim() || undefined, - config: r.config && Object.keys(r.config).length ? r.config : undefined, - })), + stimulus_component_kinds: + stimulusKindsFromSchema.length > 0 ? stimulusKindsFromSchema : [...draft.stimulus_component_kinds], + response_component_kinds: + responseKindsFromSchema.length > 0 ? responseKindsFromSchema : [...draft.response_component_kinds], + stimulus_schema, + response_schema, } }