qustion type builder integartion fix
This commit is contained in:
parent
457d09f02b
commit
9b35a8bf30
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user