ADAPT-3785: Fix storyboard Word/PDF export formatting, MCQ title, and… - #311
ADAPT-3785: Fix storyboard Word/PDF export formatting, MCQ title, and…#311lokeshece1 wants to merge 4 commits into
Conversation
… PDF overlap Word export: embed image bytes via new assetResolver; strip HTML from MCQ option text and feedback; match Storyboard Preview layout (type badge, bulleted options with correctness glyphs, indented feedback, italic submit hint). PDF export: pdfDrawImage helper caps width to content area, preserves aspect, page-break aware, advances doc.y after draw (pdfkit doc.image does NOT move the cursor - root cause of text-over-image overlap). pdfResetText between blocks; pdfEnsureRoom before component/assessment headers; removed all continued:true chains; bufferPages + lineGap(2) + moveDown(0.8). Verified: 56 pages / 40 images / 1010 text runs / 0 overlaps via pdfjs bbox analysis. MCQ title mapping: question body IS the MCQ title (stored as displayTitle); block-title is fallback header; body paragraph only shown when it differs from header. validateAssessment accepts blockTitle. Save writes both title/displayTitle and dedupes body. Load reads displayTitle first, keeps props.title only when distinct. DEFAULT_SCHEMA_TITLES filter blocks 'New Component Title'/'Article title' from UI and export.
There was a problem hiding this comment.
Pull request overview
This PR targets ADAPT-3785 by aligning the Storyboard’s on-screen Preview, course projection, and Word/PDF exports—especially around spacing, placeholder-title suppression, assessment rendering, and embedding real asset bytes in exports.
Changes:
- Reworked server-side Word/PDF export to embed images/posters, improve spacing, and render assessments/options/feedback more faithfully.
- Updated Storyboard UI flows to avoid leaking schema-default placeholder titles, improve Preview defaults, and ensure Export persists the live editor document.
- Adjusted course/storyboard generation and bootstrap/title plumbing to reduce duplication and better match learner-facing output.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/content/storyboard/utils/documentConvert.js | Major refactor of docx/pdf export rendering (headings, components, assessments, images, spacing). |
| plugins/content/storyboard/utils/assetResolver.js | New server-side resolver to load asset bytes/dimensions for export embedding. |
| plugins/content/storyboard/routes/requestHandlers.js | Pass explicit user/tenant context into export conversion for asset resolution. |
| new-ui-source/src/types/storyboard.ts | Adjust assessment validation signature/behavior and clarify assessment title/body expectations. |
| new-ui-source/src/pages/StoryboardPage.tsx | Fetch course title when navigation state is missing to avoid stale/placeholder titles. |
| new-ui-source/src/pages/SetupPage.tsx | Hide duplicated Setup chrome when in Storyboard mode. |
| new-ui-source/src/index.css | Minor styling updates for the Storyboard toolbar button variant. |
| new-ui-source/src/hooks/useStoryboard.ts | Allow saving an explicit document snapshot (used by export flow). |
| new-ui-source/src/components/storyboard/StoryboardWorkspace.tsx | Strip placeholder headings, persist live editor content before export, and suppress placeholder course titles. |
| new-ui-source/src/components/storyboard/DocumentToolbar.tsx | Toolbar layout change (single-row with right-aligned refresh/AI actions). |
| new-ui-source/src/components/storyboard/blocks/componentBlock.tsx | Default to collapsed Preview for non-empty components (expand only for new/blank). |
| new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx | Expanded collapsed Preview rendering (options/feedback/footer) and updated validation call. |
| new-ui-source/src/api/storyboardGeneration.ts | Adjust assessment title/body mapping and block layout selection (full vs left/right). |
| new-ui-source/src/api/adaptAuthoring.ts | Centralize placeholder-title handling and improve storyboard block projection rules. |
Suppressed comments (2)
plugins/content/storyboard/utils/documentConvert.js:924
- Same duplication issue as the docx exporter:
headerTextprefersquestion, butshowQuestionParagraphstill printsquestionagain when a distincttitleexists, resulting in repeated question text in the PDF output.
// See docx-side comment: question body is the MCQ Title when present.
const headerText = question || title;
const showQuestionParagraph = !!question && !!title && question !== title;
doc.font('Helvetica-Bold').fontSize(11).text(`${kindLabel}${headerText ? ' — ' : ''}${headerText}`);
plugins/content/storyboard/utils/documentConvert.js:1049
- PDF export has the same issue as docx:
numberedListItemblocks are rendered with a bullet (•) prefix, so ordered lists lose numbering in the PDF output.
} else if (b.type === 'bulletListItem' || b.type === 'numberedListItem') {
const text = inlineToText(b.content);
if (text) doc.font('Helvetica').fontSize(11).text(`• ${text}`, { indent: 12 });
} else {
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@lokeshece1 Update title and address co-pilot comments. |
…ered lists, storyboard helpers moved out of common files - Assessment Title/Body precedence unified to blockTitle || question across docx/pdf export, collapsed preview, and generation (body only rendered when distinct from the header, so text never duplicates and a distinct Body is no longer unreachable) - numberedListItem exports with sequential 1. 2. 3. numbering (docx + pdf), counter resets between list runs - Placeholder-title helpers relocated from api/adaptAuthoring.ts to components/storyboard/placeholderTitles.ts; bootstrap title change reverted; Topic/Section/Content Group terminology added client- and server-side - validateAssessment docstring updated to the Title-primary hydration
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
new-ui-source/src/components/storyboard/blocks/componentBlock.tsx:218
- hasComponentContent() assumes groupedContent item fields are always strings (calling .trim()), and it ignores assetId-only selections for image/media refs. If a persisted document contains partial item objects (or an asset picker stores only assetId/link later), this can throw or misclassify a non-empty component as blank, changing the initial collapsed state incorrectly.
function hasComponentContent(kind: ComponentKind, data: ComponentData, title: string): boolean {
if (title.trim() || data.description.trim() || data.instruction.trim()) return true;
switch (kind) {
case 'groupedContent':
return (data.items || []).some((it) => it.title.trim() || it.body.trim() || it.image.trim());
case 'image':
return !!(data.image?.link || data.image?.url);
case 'video':
case 'audio':
return !!(data.media?.asset?.link || data.media?.asset?.url || data.media?.poster?.link || data.media?.poster?.url);
case 'h5p':
return !!(data.media?.asset?.link || data.media?.asset?.url);
case 'laerdalForm':
return (data.fields || []).some((f) => f.label.trim());
case 'assessmentResult':
return !!(data.result?.assessmentId.trim() || (data.result?.bands || []).some((b) => b.feedback.trim()));
default:
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Suppressed comments (7)
new-ui-source/src/components/storyboard/blocks/componentBlock.tsx:205
- For image components, hasComponentContent() ignores image.assetId. If the selected asset only populates assetId (common for DAM-picked assets), the block is misclassified as blank and opens in edit mode on reload.
case 'image':
return !!(data.image?.link || data.image?.url);
new-ui-source/src/components/storyboard/blocks/componentBlock.tsx:208
- For video/audio components, hasComponentContent() ignores DAM asset ids (asset.assetId / poster.assetId). This can misclassify existing media content as blank, opening the card expanded unexpectedly.
case 'video':
case 'audio':
return !!(data.media?.asset?.link || data.media?.asset?.url || data.media?.poster?.link || data.media?.poster?.url);
new-ui-source/src/components/storyboard/blocks/componentBlock.tsx:210
- For H5P components, hasComponentContent() ignores media.asset.assetId, so DAM-picked H5P sources can be treated as blank.
case 'h5p':
return !!(data.media?.asset?.link || data.media?.asset?.url);
new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx:405
- The Preview-mode filters treat any non-empty string as authored, so legacy HTML like "" passes
.trim()and produces empty-looking rows/options in the preview. Consider stripping tags for the preview-only “displayed*” filters.
const displayedFeedback = feedbackRows.filter(([k]) => fb[k] && fb[k].trim());
const displayedOptions = (model.options ?? []).filter((o) => o.text.trim());
const displayedItems = (model.items ?? []).filter((i) => i && i.trim());
const displayedPairs = (model.pairs ?? []).filter((p) => p && (p.prompt || p.answer));
const displayedAnswers = (model.answers ?? []).filter((a) => a && a.trim());
new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx:463
- In Preview mode, per-option feedback is rendered as raw text. If legacy records contain HTML (e.g. "
Correct
"), the tags will display literally and won’t match the Word/PDF export (which strips HTML).
{o.feedback && o.feedback.trim() && (
<div className="ml-5 mt-0.5 text-[13px] italic text-muted-foreground">
{o.feedback}
</div>
)}
new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx:518
- In Preview mode, whole-question feedback is rendered without stripping HTML, so legacy values like "
Incorrect
" will show tags and can create blank-looking rows.
<div key={k}>
<span className="font-semibold text-foreground">{lbl}:</span>{' '}
<span className="text-foreground">{fb[k]}</span>
</div>
new-ui-source/src/components/storyboard/placeholderTitles.ts:50
- placeholder-title detection is case-sensitive here, but the server-side export filter is case-insensitive (lowercases before matching). This mismatch can let placeholder titles with different casing leak into the storyboard header/export on the client side.
export function isDefaultSchemaTitle(text: string | undefined | null): boolean {
const t = (text || '').trim();
return !t || DEFAULT_SCHEMA_TITLES.has(t);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
new-ui-source/src/components/storyboard/placeholderTitles.ts:50
isDefaultSchemaTitle/storyboardLabelare currently case-sensitive, but the server-side placeholder-title filter is explicitly case-insensitive (lowercases before lookup). As a result, legacy variants like "Article title" / "article title" can slip through in the UI even though they’re suppressed in export, breaking the "kept in sync" contract.
export function isDefaultSchemaTitle(text: string | undefined | null): boolean {
const t = (text || '').trim();
return !t || DEFAULT_SCHEMA_TITLES.has(t);
}
new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx:452
- Assessment preview renders option text/feedback verbatim. The backend can store these fields as HTML (e.g.
<p>Correct</p>), so the Preview will show raw tags. Since the Word/PDF export path already strips HTML for these values, the UI preview should also strip tags for consistency/readability.
<span className={o.correct ? 'font-medium text-foreground' : 'text-foreground'}>
{o.text}
</span>
</div>
{kind === 'gmcq' && (o.imageUrl || o.image) && (
new-ui-source/src/components/storyboard/blocks/assessmentBlock.tsx:518
- Whole-question feedback is also rendered verbatim in Preview; if stored as HTML it will display raw tags. Strip tags here the same way as option feedback so the Preview matches the exported document.
<div key={k}>
<span className="font-semibold text-foreground">{lbl}:</span>{' '}
<span className="text-foreground">{fb[k]}</span>
</div>
✅ PR Completion Checklist
Prefix: ADAPT-XXXX Brief descriptionbower.json(if applicable)npm run test-e2e-dev-pipelineexecuted and passingContext
Resolves / Addresses ADAPT-3785
Fixed the UI storyboard