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>
This commit is contained in:
Sem Bauke
2026-01-16 15:30:40 +01:00
committed by GitHub
parent a49913fff2
commit a8fc3ba586
3 changed files with 158 additions and 15 deletions
@@ -45,6 +45,8 @@ type BlockInfo = {
type SuperBlockInfo = {
blocks: Record<string, BlockInfo>;
chapters?: Record<string, string>;
modules?: Record<string, string>;
};
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
@@ -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<IntroJson>(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<SuperBlocks, string>
)[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(
@@ -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',
@@ -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[];
}[];
}[];