FX-8: stop dropping / mis-attaching response areas on save
serializeCanvasShapes recomputed each region's owner purely from geometry, ignoring the persisted question_id: a region with no enclosing part (or any region when a template had no parts) was silently dropped, and one overflowing its part by a pixel re-parented to parts[0]. Resolve the owner in order: geometric containment (a drag into a part re-attaches) -> persisted question_id if it still points at a saved question (survives drift / no-container) -> nearest part on the page -> first part -> first question of any kind. exam_response_areas.question_id is NOT NULL, so a region is never dropped while any question exists. Tests: never drops a region with no enclosing part; persisted question wins over geometric nearest. Full model.test.ts suite (6) passes; tsc clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7bd66fbaf0
commit
4001772d5e
@@ -147,11 +147,22 @@ export function serializeCanvasShapes(template: ExamTemplateDetail, shapes: Exam
|
||||
questions.push({ id: qid, parent_id: parentBand?.questionId ?? null, label: part.label || `Part ${index + 1}`, order: index, max_marks: Number(part.maxMarks ?? 0), answer_type: part.answerType ?? 'written', mcq_options: null, mark_scheme: {}, is_container: false, spec_ref: null, bounds: bounds(part), page: pageForShape(part, pages), source: persistedSource(part), confirmed: persistedConfirmed(part), confidence: persistedConfidence(part), derivation: persistedDerivation(part) })
|
||||
})
|
||||
|
||||
// Resolve each region's owner question. Order: current geometric containment (a user who drags a
|
||||
// region into a part re-attaches it) → the PERSISTED attachment if it still points at a saved
|
||||
// question (survives pixel-overflow drift and regions with no enclosing part) → nearest part on the
|
||||
// page → first part → first question of any kind. exam_response_areas.question_id is NOT NULL, so we
|
||||
// never silently drop a region while any question exists (only a template with zero questions can).
|
||||
const questionIds = new Set(questions.map((q) => q.id))
|
||||
const response_areas: TemplateReplacePayload['response_areas'] = []
|
||||
for (const region of regions) {
|
||||
const containingPart = parts.find((part) => contains(bounds(part), bounds(region)))
|
||||
const fallbackPart = parts.find((part) => pageForShape(part, pages) === pageForShape(region, pages)) ?? parts[0]
|
||||
const questionId = containingPart ? partQuestionIds.get(containingPart.id) : fallbackPart ? partQuestionIds.get(fallbackPart.id) : undefined
|
||||
const persisted = isUuid(region.questionId) && questionIds.has(region.questionId) ? region.questionId : undefined
|
||||
const nearestPart = parts.find((part) => pageForShape(part, pages) === pageForShape(region, pages)) ?? parts[0]
|
||||
const questionId =
|
||||
(containingPart && partQuestionIds.get(containingPart.id))
|
||||
|| persisted
|
||||
|| (nearestPart && partQuestionIds.get(nearestPart.id))
|
||||
|| questions[0]?.id
|
||||
if (!questionId) continue
|
||||
const kind = region.kind as ExamCanvasRegionKind
|
||||
response_areas.push({ id: isUuid(region.id) ? region.id : newDomainId(), question_id: questionId, page: pageForShape(region, pages), bounds: bounds(region), kind, response_form: kind === 'response' ? (region.responseForm ?? 'lines') : null, context_type: kind === 'context' ? (region.contextType ?? 'generic') : null, source: persistedSource(region), confirmed: persistedConfirmed(region), confidence: persistedConfidence(region), mark_subtype: null, derivation: persistedDerivation(region) })
|
||||
|
||||
Reference in New Issue
Block a user