From a8fc3ba58603aa83486c0cbad4c9120625bb7460 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Fri, 16 Jan 2026 15:30:40 +0100 Subject: [PATCH] feat: option to create a new chapter/module when creating new blocks for language curricula (#65251) Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> --- .../create-language-block.ts | 167 ++++++++++++++++-- .../helpers/create-project.test.ts | 2 + .../helpers/create-project.ts | 4 + 3 files changed, 158 insertions(+), 15 deletions(-) diff --git a/tools/challenge-helper-scripts/create-language-block.ts b/tools/challenge-helper-scripts/create-language-block.ts index 0f6bb23164c..579f0e7e4af 100644 --- a/tools/challenge-helper-scripts/create-language-block.ts +++ b/tools/challenge-helper-scripts/create-language-block.ts @@ -45,6 +45,8 @@ type BlockInfo = { type SuperBlockInfo = { blocks: Record; + chapters?: Record; + modules?: Record; }; type IntroJson = Record; @@ -56,6 +58,10 @@ interface CreateBlockArgs { title?: string; chapter?: string; module?: string; + newChapterName?: string; + newChapterTitle?: string; + newModuleName?: string; + newModuleTitle?: string; position?: number; blockLabel?: BlockLabel; blockLayout?: string; @@ -69,6 +75,8 @@ async function createLanguageBlock( title?: string, chapter?: string, module?: string, + chapterTitle?: string, + moduleTitle?: string, position?: number, blockLabel?: BlockLabel, blockLayout?: string, @@ -77,7 +85,15 @@ async function createLanguageBlock( if (!title) { title = block; } - await updateIntroJson(superBlock, block, title); + await updateIntroJson({ + superBlock, + block, + title, + chapter, + module, + chapterTitle, + moduleTitle + }); const challengeLang = getLangFromSuperBlock(superBlock); let challengeId: ObjectId; @@ -132,20 +148,52 @@ async function createLanguageBlock( await createIntroMD(superBlock, block, title); } -async function updateIntroJson( - superBlock: SuperBlocks, - block: string, - title: string -) { +async function updateIntroJson({ + superBlock, + block, + title, + chapter, + module, + chapterTitle, + moduleTitle +}: { + superBlock: SuperBlocks; + block: string; + title: string; + chapter?: string; + module?: string; + chapterTitle?: string; + moduleTitle?: string; +}) { const introJsonPath = path.resolve( __dirname, '../../client/i18n/locales/english/intro.json' ); const newIntro = await parseJson(introJsonPath); + newIntro[superBlock].blocks[block] = { title, intro: ['', ''] }; + + if (chapter && chapterTitle) { + if (!newIntro[superBlock].chapters) { + newIntro[superBlock].chapters = {}; + } + if (!newIntro[superBlock].chapters[chapter]) { + newIntro[superBlock].chapters[chapter] = chapterTitle; + } + } + + if (module && moduleTitle) { + if (!newIntro[superBlock].modules) { + newIntro[superBlock].modules = {}; + } + if (!newIntro[superBlock].modules[module]) { + newIntro[superBlock].modules[module] = moduleTitle; + } + } + void withTrace( fs.writeFile, introJsonPath, @@ -387,16 +435,54 @@ void getAllBlocks() modules: { dashedName: string; blocks: string[] }[]; }[]; }; - return structure.chapters.map(chapter => chapter.dashedName); + return [ + ...structure.chapters.map(chapter => chapter.dashedName), + '-- Create new chapter --' + ]; }, when: (answers: CreateBlockArgs) => chapterBasedSuperBlocks.includes(answers.superBlock) }, + { + name: 'newChapterName', + message: 'Enter the dashed name for the new chapter (in kebab-case):', + validate: (name: string) => { + if (!name || name.trim() === '') { + return 'Chapter name cannot be empty.'; + } + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) { + return 'Chapter name must be in kebab-case (e.g., "chapter-one").'; + } + return true; + }, + filter: (name: string) => name.toLowerCase().trim(), + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) && + answers.chapter === '-- Create new chapter --' + }, + { + name: 'newChapterTitle', + message: 'Enter the title for the new chapter:', + default: ({ newChapterName }: { newChapterName: string }) => + newChapterName, + validate: (title: string) => { + if (!title || title.trim() === '') { + return 'Chapter title cannot be empty.'; + } + return true; + }, + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) && + answers.chapter === '-- Create new chapter --' + }, { name: 'module', message: 'What module should this language block go in?', type: 'list', choices: (answers: CreateBlockArgs) => { + if (answers.chapter === '-- Create new chapter --') { + return ['-- Create new module --']; + } const superblockFilename = ( superBlockToFilename as Record )[answers.superBlock]; @@ -406,18 +492,50 @@ void getAllBlocks() modules: { dashedName: string; blocks: string[] }[]; }[]; }; - return ( + const existingModules = structure.chapters .find(chapter => chapter.dashedName === answers.chapter) - ?.modules.map(module => module.dashedName) ?? [] - ); + ?.modules.map(module => module.dashedName) ?? []; + return [...existingModules, '-- Create new module --']; }, when: (answers: CreateBlockArgs) => chapterBasedSuperBlocks.includes(answers.superBlock) }, + { + name: 'newModuleName', + message: 'Enter the dashed name for the new module (in kebab-case):', + validate: (name: string) => { + if (!name || name.trim() === '') { + return 'Module name cannot be empty.'; + } + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name.trim())) { + return 'Module name must be in kebab-case (e.g., "module-one").'; + } + return true; + }, + filter: (name: string) => name.toLowerCase().trim(), + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) && + answers.module === '-- Create new module --' + }, + { + name: 'newModuleTitle', + message: 'Enter the title for the new module:', + default: ({ newModuleName }: { newModuleName: string }) => + newModuleName, + validate: (title: string) => { + if (!title || title.trim() === '') { + return 'Module title cannot be empty.'; + } + return true; + }, + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) && + answers.module === '-- Create new module --' + }, { name: 'position', - message: 'At which position does this appear in the module?', + message: 'At which position does this new block appear in the module?', default: 1, validate: (position: string) => { return parseInt(position, 10) > 0 @@ -440,23 +558,42 @@ void getAllBlocks() title, chapter, module, + newChapterName, + newChapterTitle, + newModuleName, + newModuleTitle, position, blockLabel, blockLayout, questionCount - }: CreateBlockArgs) => + }: CreateBlockArgs) => { + const resolvedChapter = + chapter === '-- Create new chapter --' ? newChapterName : chapter; + const resolvedModule = + module === '-- Create new module --' ? newModuleName : module; + + // Only pass chapter title if we're creating a new chapter + const chapterTitle = + chapter === '-- Create new chapter --' ? newChapterTitle : undefined; + // Only pass module title if we're creating a new module + const moduleTitle = + module === '-- Create new module --' ? newModuleTitle : undefined; + await createLanguageBlock( superBlock, block, helpCategory, title, - chapter, - module, + resolvedChapter, + resolvedModule, + chapterTitle, + moduleTitle, position, blockLabel, blockLayout, questionCount - ) + ); + } ) .then(() => console.log( diff --git a/tools/challenge-helper-scripts/helpers/create-project.test.ts b/tools/challenge-helper-scripts/helpers/create-project.test.ts index f2a11a3d913..b99bd9a8842 100644 --- a/tools/challenge-helper-scripts/helpers/create-project.test.ts +++ b/tools/challenge-helper-scripts/helpers/create-project.test.ts @@ -156,6 +156,7 @@ describe('updateChapterModuleSuperblockStructure', () => { }, { dashedName: 'module2c1', + comingSoon: true, blocks: ['block2'] } ] @@ -189,6 +190,7 @@ describe('updateChapterModuleSuperblockStructure', () => { chapters: [ { dashedName: 'chapter2', + comingSoon: true, modules: [ { dashedName: 'module1c2', diff --git a/tools/challenge-helper-scripts/helpers/create-project.ts b/tools/challenge-helper-scripts/helpers/create-project.ts index 4255cb19503..caed2b13da4 100644 --- a/tools/challenge-helper-scripts/helpers/create-project.ts +++ b/tools/challenge-helper-scripts/helpers/create-project.ts @@ -29,6 +29,7 @@ export async function updateSimpleSuperblockStructure( function createNewChapter(chapter: string, module: string, block: string) { return { dashedName: chapter, + comingSoon: true, modules: [ { dashedName: module, @@ -41,6 +42,7 @@ function createNewChapter(chapter: string, module: string, block: string) { function createNewModule(module: string, block: string) { return { dashedName: module, + comingSoon: true, blocks: [block] }; } @@ -48,8 +50,10 @@ function createNewModule(module: string, block: string) { export type ChapterModuleSuperblockStructure = { chapters: { dashedName: string; + comingSoon?: boolean; modules: { dashedName: string; + comingSoon?: boolean; blocks: string[]; }[]; }[];