qustion type builder integartion fix

This commit is contained in:
Yared Yemane 2026-05-13 04:38:09 -07:00
parent 457d09f02b
commit 9b35a8bf30
3 changed files with 209 additions and 46 deletions

View File

@ -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<QuestionTypeDefinition | undefined> {
const res = await http.get<ApiEnvelope<unknown>>(`/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(

View File

@ -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<number, string> {
const prefix = `${side}_`
const out: Record<number, string> = {}
@ -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 (
<div className="space-y-8 pb-32">
<Card className="max-w-6xl mx-auto overflow-hidden border border-grayScale-200 shadow-sm rounded-2xl bg-white">
@ -116,26 +217,43 @@ export function QuestionTypeConfigStep({
Section A: Question input types
</h3>
<p className="text-[14px] text-grayScale-500 mt-1 font-medium">
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{" "}
<code className="text-[11px] bg-grayScale-100 px-1 rounded">stimulus_component_kinds</code>{" "}
while <code className="text-[11px] bg-grayScale-100 px-1 rounded">stimulus_schema</code> can
include the same kind multiple times (different ids).
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{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 (
<div key={kind} className="space-y-2">
<ComponentKindCard
key={kind}
label={label}
Icon={Icon}
selected={selected}
onClick={() =>
setDraft((d) => ({
...d,
stimulus_component_kinds: toggleKind(d.stimulus_component_kinds, kind),
}))
}
onClick={() => handleStimulusKindClick(kind)}
/>
{selected ? (
<div className="flex items-center justify-between gap-2 px-0.5 min-h-[32px]">
<span className="text-[12px] text-grayScale-500 font-medium">
{slotCount} slot{slotCount === 1 ? "" : "s"}
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 shrink-0 text-[12px] font-bold text-[#9E2891] hover:text-[#8A237E] hover:bg-violet-50"
onClick={() => addStimulusSlot(kind)}
>
<Plus className="h-3.5 w-3.5 mr-1" />
Add slot
</Button>
</div>
) : null}
</div>
)
})}
</div>
@ -150,26 +268,43 @@ export function QuestionTypeConfigStep({
Section B: Answer types
</h3>
<p className="text-[14px] text-grayScale-500 mt-1 font-medium">
How should the student answer this question?
How should the student answer?{" "}
<code className="text-[11px] bg-grayScale-100 px-1 rounded">response_component_kinds</code> is
deduplicated; use <span className="font-medium text-grayScale-600">Add slot</span> for multiple
fields of the same kind.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{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 (
<div key={kind} className="space-y-2">
<ComponentKindCard
key={kind}
label={label}
Icon={Icon}
selected={selected}
onClick={() =>
setDraft((d) => ({
...d,
response_component_kinds: toggleKind(d.response_component_kinds, kind),
}))
}
onClick={() => handleResponseKindClick(kind)}
/>
{selected ? (
<div className="flex items-center justify-between gap-2 px-0.5 min-h-[32px]">
<span className="text-[12px] text-grayScale-500 font-medium">
{slotCount} slot{slotCount === 1 ? "" : "s"}
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 shrink-0 text-[12px] font-bold text-[#9E2891] hover:text-[#8A237E] hover:bg-violet-50"
onClick={() => addResponseSlot(kind)}
>
<Plus className="h-3.5 w-3.5 mr-1" />
Add slot
</Button>
</div>
) : null}
</div>
)
})}
</div>

View File

@ -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<string>()
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,
}
}