Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions localtypings/pxtarget.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,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<pxt.PackageConfig>;
hiddenNamespaces?: string[]; // list of categories to put in the toolbox filters of the tutorial project's pxt.json
}

interface TutorialMetadata {
Expand All @@ -1249,6 +1250,7 @@ declare namespace pxt.tutorial {
preferredEditor?: string; // preferred editor for opening the tutorial
hideDone?: boolean; // Do not show a "Done" button at the end of the tutorial
hideFromProjects?: boolean; // hide this tutorial from the projects list
hideReplaceMyCode?: boolean; // hide the "Replace Code" button in the tutorial
}

interface TutorialBlockConfigEntry {
Expand Down Expand Up @@ -1347,6 +1349,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<pxt.PackageConfig>;
hiddenNamespaces?: string[]; // list of categories to put in the toolbox filters of the tutorial project's pxt.json
}
interface TutorialCompletionInfo {
// id of the tutorial
Expand Down
19 changes: 14 additions & 5 deletions pxtlib/tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
Expand Down Expand Up @@ -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++
Expand All @@ -163,7 +170,8 @@ namespace pxt.tutorial {
jres,
assetJson,
customTs,
simThemeJson
simThemeJson,
hiddenNamespaces
};

function checkTutorialEditor(expected: string) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -479,6 +487,7 @@ ${code}
globalBlockConfig: tutorialInfo.globalBlockConfig,
globalValidationConfig: tutorialInfo.globalValidationConfig,
simTheme: tutorialInfo.simTheme,
hiddenNamespaces: tutorialInfo.hiddenNamespaces,
};

return { options: tutorialOptions, editor: tutorialInfo.editor };
Expand Down
76 changes: 52 additions & 24 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1776,6 +1776,7 @@ export class ProjectView
await this.loadTutorialCustomTsAsync();
await this.loadTutorialTemplateCodeAsync();
await this.loadTutorialBlockConfigsAsync();
await this.loadTutorialHiddenCategoriesAsync();

const main = pkg.getEditorPkg(pkg.mainPkg);

Expand Down Expand Up @@ -2103,8 +2104,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;
Expand All @@ -2115,33 +2119,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
}
}
}

Expand Down Expand Up @@ -2243,6 +2250,27 @@ export class ProjectView
return Promise.resolve();
}

private async loadTutorialHiddenCategoriesAsync(): Promise<void> {
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<void> {
const mainPkg = pkg.mainEditorPkg();
const header = mainPkg.header;
Expand Down
9 changes: 7 additions & 2 deletions webapp/src/components/tutorial/TutorialContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div className="tutorial-container" ref={containerRef}>
{!isHorizontal && stepCounter}
<div className={classList("tutorial-content", hasHint && "has-hint")} ref={contentRef} onScroll={updateScrollGradient}>
Expand All @@ -283,8 +287,9 @@ export function TutorialContainer(props: TutorialContainerProps) {
tutorialId={tutorialId}
currentStep={currentStep}
attemptsWithError={stepErrorAttemptCount} />}
{hasTemplate && currentStep == firstNonModalStep && preferredEditor !== "asset" && !pxt.appTarget.appTheme.hideReplaceMyCode &&
<TutorialResetCode tutorialId={tutorialId} currentStep={visibleStep} resetTemplateCode={parent.resetTutorialTemplateCode} />}
{showReplaceMyCode &&
<TutorialResetCode tutorialId={tutorialId} currentStep={visibleStep} resetTemplateCode={parent.resetTutorialTemplateCode} />
}
{showScrollGradient && <div className="tutorial-scroll-gradient" />}
{isModal && !hideModal && <Modal isOpen={isModal} closeIcon={false} header={currentStepInfo.title || name} buttons={modalActions}
className="hintdialog" onClose={onModalClose} dimmer={true}
Expand Down
23 changes: 20 additions & 3 deletions webapp/src/toolboxeditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,33 @@ export abstract class ToolboxEditor extends srceditor.Editor {
}

protected shouldShowCustomCategory(ns: string) {
const filters = this.parent.state.editorState && this.parent.state.editorState.filters;
let filters = this.parent.state.editorState && this.parent.state.editorState.filters;

const projectFilter = getProjectToolboxFilters();

if (projectFilter) {
if (filters) {
// tutorial filters override project filters
pxt.U.jsonMergeFrom(projectFilter, filters);
}

filters = projectFilter;
}

if (filters) {
// These categories are special and won't have any children so we need to check the filters manually
if (ns === "variables" && (!filters.blocks ||
filters.blocks["variables_set"] ||
filters.blocks["variables_get"] ||
filters.blocks["variables_change"]) &&
(!filters.namespaces || filters.namespaces["variables"] !== pxt.editor.FilterState.Disabled)) {
(!filters.namespaces || !shouldHideCategory("variables", filters.namespaces))) {
return true;
} else if (ns === "functions" && (!filters.blocks ||
filters.blocks["function_definition"] ||
filters.blocks["function_call"] ||
filters.blocks["procedures_defnoreturn"] ||
filters.blocks["procedures_callnoreturn"]) &&
(!filters.namespaces || filters.namespaces["functions"] !== pxt.editor.FilterState.Disabled)) {
(!filters.namespaces || !shouldHideCategory("functions", filters.namespaces))) {
return true;
} else {
return false;
Expand Down Expand Up @@ -412,3 +424,8 @@ export abstract class ToolboxEditor extends srceditor.Editor {

onToolboxBlur(e: React.FocusEvent, keepFlyoutOpen: boolean) {};
}


function shouldHideCategory(category: string, filters: {[index: string]: pxt.editor.FilterState}): boolean {
return filters[category] == pxt.editor.FilterState.Hidden || filters[category] == pxt.editor.FilterState.Disabled;
}