diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index eca6c35026dc..6465b7110e3d 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -1204,6 +1204,7 @@ declare namespace pxt.tutorial { globalBlockConfig?: TutorialBlockConfig; // concatenated `blockconfig.global` sections. Contains block configs applicable to all tutorial steps globalValidationConfig?: CodeValidationConfig; // concatenated 'validation.global' sections. Contains validation config applicable to all steps simTheme?: Partial; + hiddenNamespaces?: string[]; // list of categories to put in the toolbox filters of the tutorial project's pxt.json } interface TutorialMetadata { @@ -1219,6 +1220,7 @@ declare namespace pxt.tutorial { autoexpandOff?: boolean; // INTERNAL TESTING ONLY preferredEditor?: string; // preferred editor for opening the tutorial hideDone?: boolean; // Do not show a "Done" button at the end of the tutorial + hideReplaceMyCode?: boolean; // hide the "Replace Code" button in the tutorial } interface TutorialBlockConfigEntry { @@ -1317,6 +1319,7 @@ declare namespace pxt.tutorial { globalBlockConfig?: TutorialBlockConfig; // concatenated `blockconfig.global` sections. Contains block configs applicable to all tutorial steps globalValidationConfig?: CodeValidationConfig // concatenated 'validation.global' sections. Contains validation config applicable to all steps simTheme?: Partial; + hiddenNamespaces?: string[]; // list of categories to put in the toolbox filters of the tutorial project's pxt.json } interface TutorialCompletionInfo { // id of the tutorial diff --git a/pxtlib/tutorial.ts b/pxtlib/tutorial.ts index 2debab43b11b..07305ca3e84b 100644 --- a/pxtlib/tutorial.ts +++ b/pxtlib/tutorial.ts @@ -19,7 +19,8 @@ namespace pxt.tutorial { jres, assetJson, customTs, - simThemeJson + simThemeJson, + hiddenNamespaces } = computeBodyMetadata(body); // For python HOC, hide the toolbox (we don't support flyoutOnly mode). @@ -66,12 +67,13 @@ namespace pxt.tutorial { customTs, globalBlockConfig, globalValidationConfig, - simTheme + simTheme, + hiddenNamespaces }; } export function getMetadataRegex(): RegExp { - return /``` *(sim|block|blocks|filterblocks|spy|ghost|typescript|ts|js|javascript|template|python|jres|assetjson|customts|simtheme|python-template|ts-template|typescript-template|js-template|javascript-template)\s*\n([\s\S]*?)\n```/gmi; + return /``` *(sim|block|blocks|filterblocks|spy|ghost|typescript|ts|js|javascript|template|python|jres|assetjson|customts|simtheme|python-template|ts-template|typescript-template|js-template|javascript-template|hiddennamespaces)\s*\n([\s\S]*?)\n```/gmi; } function computeBodyMetadata(body: string) { @@ -88,6 +90,7 @@ namespace pxt.tutorial { let assetJson: string; let customTs: string; let simThemeJson: string; + let hiddenNamespaces: string[]; // Concatenate all blocks in separate code blocks and decompile so we can detect what blocks are used (for the toolbox) body .replace(/((?!.)\s)+/g, "\n") @@ -147,6 +150,10 @@ namespace pxt.tutorial { customTs = m2; m2 = ""; break; + case "hiddennamespaces": + hiddenNamespaces = (m2 as string).split(/\s/m).map(s => s.trim()).filter(s => !!s); + m2 = ""; + break; } code.push(language === "python" ? `\n${m2}\n` : `{\n${m2}\n}`); idx++ @@ -163,7 +170,8 @@ namespace pxt.tutorial { jres, assetJson, customTs, - simThemeJson + simThemeJson, + hiddenNamespaces }; function checkTutorialEditor(expected: string) { @@ -387,7 +395,7 @@ ${code} /* Remove hidden snippets from text */ function stripHiddenSnippets(str: string): string { if (!str) return str; - const hiddenSnippetRegex = /```(filterblocks|package|ghost|config|template|jres|assetjson|simtheme|customts|blockconfig\.local|blockconfig\.global|validation\.local|validation\.global)\s*\n([\s\S]*?)\n```/gmi; + const hiddenSnippetRegex = /```(filterblocks|package|ghost|config|template|jres|assetjson|simtheme|customts|hiddennamespaces|blockconfig\.local|blockconfig\.global|validation\.local|validation\.global)\s*\n([\s\S]*?)\n```/gmi; return str.replace(hiddenSnippetRegex, '').trim(); } @@ -479,6 +487,7 @@ ${code} globalBlockConfig: tutorialInfo.globalBlockConfig, globalValidationConfig: tutorialInfo.globalValidationConfig, simTheme: tutorialInfo.simTheme, + hiddenNamespaces: tutorialInfo.hiddenNamespaces, }; return { options: tutorialOptions, editor: tutorialInfo.editor }; diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 226273c3716f..fa95e5439a09 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -1722,6 +1722,7 @@ export class ProjectView await this.loadTutorialCustomTsAsync(); await this.loadTutorialTemplateCodeAsync(); await this.loadTutorialBlockConfigsAsync(); + await this.loadTutorialHiddenCategoriesAsync(); const main = pkg.getEditorPkg(pkg.mainPkg); @@ -2017,8 +2018,11 @@ export class ProjectView if (!header || !header.tutorial) { return; } - else if (!header.tutorial.templateCode || header.tutorial.templateLoaded) { - if (header.tutorial.mergeCarryoverCode && header.tutorial.mergeHeaderId) { + const hasCodeCarryover = header.tutorial.mergeCarryoverCode && header.tutorial.mergeHeaderId; + const hideReplaceMyCode = header.tutorial.metadata?.hideReplaceMyCode || pxt.appTarget.appTheme.hideReplaceMyCode; + + if (!header.tutorial.templateCode && !(hasCodeCarryover && hideReplaceMyCode) || header.tutorial.templateLoaded) { + if (hasCodeCarryover) { pxt.warn(lf("Refusing to carry code between tutorials because the loaded tutorial \"{0}\" does not contain a template code block.", header.tutorial.tutorial)); } return; @@ -2029,33 +2033,36 @@ export class ProjectView // Mark that the template has been loaded so that we don't overwrite the // user code if the tutorial is re-opened header.tutorial.templateLoaded = true; + let currentText = await workspace.getTextAsync(header.id); - // If we're starting in the asset editor, always load into TS - const preferredEditor = header.tutorial.metadata?.preferredEditor; - if (preferredEditor && filenameForEditor(preferredEditor) === pxt.ASSETS_FILE) { - currentText[pxt.MAIN_TS] = template; - } + if (template) { + // If we're starting in the asset editor, always load into TS + const preferredEditor = header.tutorial.metadata?.preferredEditor; + if (preferredEditor && filenameForEditor(preferredEditor) === pxt.ASSETS_FILE) { + currentText[pxt.MAIN_TS] = template; + } - const projectname = projectNameForEditor(preferredEditor || header.editor); + const projectname = projectNameForEditor(preferredEditor || header.editor); - if (projectname === pxt.PYTHON_PROJECT_NAME && header.tutorial.templateLanguage === "python") { - currentText[pxt.MAIN_PY] = template; - } - else if (projectname === pxt.JAVASCRIPT_PROJECT_NAME) { - currentText[pxt.MAIN_TS] = template; - } - else if (projectname === pxt.PYTHON_PROJECT_NAME) { - const pyCode = await compiler.decompilePythonSnippetAsync(template) - if (pyCode) { - currentText[pxt.MAIN_PY] = pyCode; + if (projectname === pxt.PYTHON_PROJECT_NAME && header.tutorial.templateLanguage === "python") { + currentText[pxt.MAIN_PY] = template; } - } - else { - const resp = await compiler.decompileBlocksSnippetAsync(template) - const blockXML = resp.outfiles[pxt.MAIN_BLOCKS]; - if (blockXML) { - currentText[pxt.MAIN_BLOCKS] = blockXML + else if (projectname === pxt.JAVASCRIPT_PROJECT_NAME) { + currentText[pxt.MAIN_TS] = template; + } + else if (projectname === pxt.PYTHON_PROJECT_NAME) { + const pyCode = await compiler.decompilePythonSnippetAsync(template) + if (pyCode) { + currentText[pxt.MAIN_PY] = pyCode; + } + } + else { + const resp = await compiler.decompileBlocksSnippetAsync(template) + const blockXML = resp.outfiles[pxt.MAIN_BLOCKS]; + if (blockXML) { + currentText[pxt.MAIN_BLOCKS] = blockXML + } } } @@ -2157,6 +2164,27 @@ export class ProjectView return Promise.resolve(); } + private async loadTutorialHiddenCategoriesAsync(): Promise { + const mainPkg = pkg.mainEditorPkg(); + const header = mainPkg.header; + if (!header || !header.tutorial || !header.tutorial.hiddenNamespaces) { + return; + } + + await mainPkg.updateConfigAsync(config => { + if (!config.toolboxFilter) { + config.toolboxFilter = { + namespaces: {}, + blocks: {} + }; + } + + for (const category of header.tutorial.hiddenNamespaces) { + config.toolboxFilter.namespaces[category] = "hidden"; + } + }); + } + async resetTutorialTemplateCode(keepAssets: boolean): Promise { const mainPkg = pkg.mainEditorPkg(); const header = mainPkg.header; diff --git a/webapp/src/components/tutorial/TutorialContainer.tsx b/webapp/src/components/tutorial/TutorialContainer.tsx index 9e6a757a699d..63e1d7a43385 100644 --- a/webapp/src/components/tutorial/TutorialContainer.tsx +++ b/webapp/src/components/tutorial/TutorialContainer.tsx @@ -257,6 +257,10 @@ export function TutorialContainer(props: TutorialContainerProps) { stepContentRef.current = ref; } + const showReplaceMyCode = + hasTemplate && currentStep == firstNonModalStep && preferredEditor !== "asset" && + !pxt.appTarget.appTheme.hideReplaceMyCode && !props.tutorialOptions.metadata?.hideReplaceMyCode + return
{!isHorizontal && stepCounter}
@@ -283,8 +287,9 @@ export function TutorialContainer(props: TutorialContainerProps) { tutorialId={tutorialId} currentStep={currentStep} attemptsWithError={stepErrorAttemptCount} />} - {hasTemplate && currentStep == firstNonModalStep && preferredEditor !== "asset" && !pxt.appTarget.appTheme.hideReplaceMyCode && - } + {showReplaceMyCode && + + } {showScrollGradient &&
} {isModal && !hideModal &&