Skip to content

Commit f49b22d

Browse files
authored
add route to load tutorial locally (microsoft#6659)
* add route to load tutorial locally * test route for tutorials * lint * updated docs * better regex * fix history * organize history by day * fix inverted rendering * tick * clear commit on changing day * only show preview for latest * lint * lint
1 parent d2b8c80 commit f49b22d

4 files changed

Lines changed: 112 additions & 45 deletions

File tree

docs/writing-docs/user-tutorials.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ You can override the markdown file from the project used for the content of the
5656
where MakeCode will load the ``filename.md`` file from the project. Don't forget to add this file in the
5757
``files`` list in ``pxt.json``.
5858

59+
### Testing
60+
61+
When looking at the diff of a markdown file (``.md``) in the GitHub view, a ``Preview as Tutorial`` will be avaiable to load through that particular file as a tutorial. Commiting to GitHub is not needed to do the local testing.
62+
5963
### Localization
6064

6165
Localized copies of the tutorial can be added to a subfolder ``_locales/[isocode]/[filename].md``

theme/github.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
.ui.header {
123123
color: @githubInvertedTextColor !important;
124124
}
125+
.ui.items>.link.item {
126+
.meta, .description {
127+
color: @githubInvertedTextColor !important;
128+
}
129+
}
125130
.diffheader {
126131
background: @mainMenuInvertedBackground !important;
127132
}

webapp/src/app.tsx

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,8 +3150,9 @@ export class ProjectView
31503150
core.hideDialog();
31513151
core.showLoading("tutorial", lf("starting tutorial..."));
31523152
sounds.initTutorial(); // pre load sounds
3153-
const scriptId = pxt.Cloud.parseScriptId(tutorialId);
3154-
const ghid = pxt.github.parseRepoId(tutorialId);
3153+
const header = workspace.getHeader(tutorialId.split(':')[0]);
3154+
const scriptId = !header && pxt.Cloud.parseScriptId(tutorialId);
3155+
const ghid = !header && !scriptId && pxt.github.parseRepoId(tutorialId);
31553156
let reportId: string = undefined;
31563157
let filename: string;
31573158
let autoChooseBoard: boolean = true;
@@ -3189,6 +3190,7 @@ export class ProjectView
31893190
core.handleNetworkError(e);
31903191
});
31913192
} else if (!!ghid && ghid.owner && ghid.project) {
3193+
pxt.tickEvent("tutorial.github");
31923194
p = pxt.packagesConfigAsync()
31933195
.then(config => {
31943196
const status = pxt.github.repoStatus(ghid, config);
@@ -3202,31 +3204,17 @@ export class ProjectView
32023204
}
32033205
return pxt.github.downloadPackageAsync(ghid.fullName, config);
32043206
})
3205-
.then(gh => {
3206-
const pxtJson = pxt.Package.parseAndValidConfig(gh.files["pxt.json"]);
3207-
// if there is any .ts file in the tutorial repo,
3208-
// add as a dependency itself
3209-
if (pxtJson.files.find(f => /\.ts/.test(f))) {
3210-
dependencies = {}
3211-
dependencies[ghid.project] = pxt.github.toGithubDependencyPath(ghid);
3212-
}
3213-
else {// just use dependencies from the tutorial
3214-
pxt.Util.jsonMergeFrom(dependencies, pxtJson.dependencies);
3215-
}
3216-
filename = pxtJson.name || lf("Untitled");
3217-
autoChooseBoard = false;
3218-
// if non-default language, find localized file if any
3219-
const mfn = (ghid.fileName || "README") + ".md";
3220-
const lang = pxt.Util.normalizeLanguageCode(pxt.Util.userLanguage());
3221-
const md =
3222-
(lang && lang[1] && gh.files[`_locales/${lang[0]}-${lang[1]}/${mfn}`])
3223-
|| (lang && lang[0] && gh.files[`_locales/${lang[0]}/${mfn}`])
3224-
|| gh.files[mfn];
3225-
return processMarkdown(md);
3226-
}).catch((e) => {
3207+
.then(gh => loadGitHubTutorial(ghid, gh.files))
3208+
.catch((e) => {
32273209
core.errorNotification(tutorialErrorMessage);
32283210
core.handleNetworkError(e);
32293211
});
3212+
} else if (header) {
3213+
pxt.tickEvent("tutorial.header");
3214+
const hghid = pxt.github.parseRepoId(header.githubId);
3215+
const hfileName = tutorialId.split(':')[1] || "README";
3216+
p = workspace.getTextAsync(header.id)
3217+
.then(script => loadGitHubTutorial(hghid, script, hfileName))
32303218
} else {
32313219
p = Promise.resolve(undefined);
32323220
}
@@ -3273,6 +3261,29 @@ export class ProjectView
32733261
features = pxt.gallery.parseFeaturesFromMarkdown(md);
32743262
return md;
32753263
}
3264+
3265+
function loadGitHubTutorial(ghid: pxt.github.ParsedRepo, files: pxt.Map<string>, fileName?: string) {
3266+
const pxtJson = pxt.Package.parseAndValidConfig(files["pxt.json"]);
3267+
// if there is any .ts file in the tutorial repo,
3268+
// add as a dependency itself
3269+
if (pxtJson.files.find(f => /\.ts$/.test(f))) {
3270+
dependencies = {}
3271+
dependencies[ghid.project] = pxt.github.toGithubDependencyPath(ghid);
3272+
}
3273+
else {// just use dependencies from the tutorial
3274+
pxt.Util.jsonMergeFrom(dependencies, pxtJson.dependencies);
3275+
}
3276+
filename = pxtJson.name || lf("Untitled");
3277+
autoChooseBoard = false;
3278+
// if non-default language, find localized file if any
3279+
const mfn = (fileName || ghid.fileName || "README") + ".md";
3280+
const lang = pxt.Util.normalizeLanguageCode(pxt.Util.userLanguage());
3281+
const md =
3282+
(lang && lang[1] && files[`_locales/${lang[0]}-${lang[1]}/${mfn}`])
3283+
|| (lang && lang[0] && files[`_locales/${lang[0]}/${mfn}`])
3284+
|| files[mfn];
3285+
return processMarkdown(md);
3286+
}
32763287
}
32773288

32783289
completeTutorialAsync(): Promise<void> {

webapp/src/gitjson.tsx

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ interface DiffCache {
2929
diff: JSX.Element;
3030
whitespace?: boolean;
3131
revert?: () => void;
32+
testAction?: {
33+
text: string;
34+
url: string;
35+
}
3236
}
3337

3438
interface GithubProps {
@@ -639,9 +643,9 @@ class GithubComponent extends data.Component<GithubProps, GithubState> {
639643
</h3>
640644
{needsCommit && <CommmitComponent parent={this} needsToken={needsToken} githubId={githubId} master={master} gs={gs} isBlocks={isBlocksMode} needsCommit={needsCommit} user={user} pullStatus={pullStatus} pullRequest={pr} />}
641645
{diffFiles && <DiffView parent={this} diffFiles={diffFiles} cacheKey={gs.commit.sha} allowRevert={true} showWhitespaceDiff={true} blocksMode={isBlocksMode} showConflicts={true} />}
646+
<HistoryZone parent={this} needsToken={needsToken} githubId={githubId} master={master} gs={gs} isBlocks={isBlocksMode} needsCommit={needsCommit} user={user} pullStatus={pullStatus} pullRequest={pr} />
642647
{master && <ReleaseZone parent={this} needsToken={needsToken} githubId={githubId} master={master} gs={gs} isBlocks={isBlocksMode} needsCommit={needsCommit} user={user} pullStatus={pullStatus} pullRequest={pr} />}
643648
{!isBlocksMode && <ExtensionZone parent={this} needsToken={needsToken} githubId={githubId} master={master} gs={gs} isBlocks={isBlocksMode} needsCommit={needsCommit} user={user} pullStatus={pullStatus} pullRequest={pr} />}
644-
<HistoryZone parent={this} needsToken={needsToken} githubId={githubId} master={master} gs={gs} isBlocks={isBlocksMode} needsCommit={needsCommit} user={user} pullStatus={pullStatus} pullRequest={pr} />
645649
<div></div>
646650
</div>
647651
</div>
@@ -731,6 +735,10 @@ class DiffView extends sui.StatelessUIElement<DiffViewProps> {
731735
{!!cache.revert && <sui.Button className="small" icon="undo" text={lf("Revert")}
732736
ariaLabel={lf("Revert file")} title={lf("Revert file")}
733737
textClass={"landscape only"} onClick={cache.revert} />}
738+
{!!cache.testAction && <sui.Link className="small button" icon="external"
739+
ariaLabel={cache.testAction.text} textClass={"landscape only"}
740+
text={cache.testAction.text} href={cache.testAction.url}
741+
target="_blank" />}
734742
{jsxEls.legendJSX}
735743
{showConflicts && !!jsxEls.conflicts && <p>{lf("Merge conflicts found. Resolve them before commiting.")}</p>}
736744
{!!cache.revert && !!deletedFiles.length &&
@@ -778,8 +786,15 @@ class DiffView extends sui.StatelessUIElement<DiffViewProps> {
778786
if (virtualF == f.file) virtualF = undefined;
779787

780788
cache.file = f
781-
if (this.props.allowRevert)
789+
if (this.props.allowRevert) {
782790
cache.revert = () => this.props.parent.revertFileAsync(f, deletedFiles, addedFiles, virtualF);
791+
if (/\.md$/.test(cache.file.name)) {
792+
cache.testAction = {
793+
text: lf("Preview as Tutorial"),
794+
url: `#tutorial:${this.props.parent.props.parent.state.header.id}:${cache.file.name.replace(/\.[a-z]+$/, '')}`
795+
}
796+
}
797+
}
783798
cache.diff = createDiff()
784799
return cache.diff;
785800
}
@@ -1253,7 +1268,7 @@ interface CommitViewProps {
12531268
githubId: pxt.github.ParsedRepo;
12541269
commit: pxt.github.CommitInfo;
12551270
expanded: boolean;
1256-
onClick?: () => void;
1271+
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
12571272
}
12581273

12591274
interface CommitViewState {
@@ -1341,8 +1356,8 @@ class CommitView extends sui.UIElement<CommitViewProps, CommitViewState> {
13411356
return <div className={`ui item link`} role="button" onClick={onClick} onKeyDown={sui.fireClickOnEnter}>
13421357
<div className="content">
13431358
{expanded && <sui.Button loading={loading} className="right floated" text={lf("Restore")} onClick={this.handleRestore} onKeyDown={sui.fireClickOnEnter} />}
1344-
<div className="header">
1345-
{date.toLocaleString()}
1359+
<div className="meta">
1360+
<span>{date.toLocaleTimeString()}</span>
13461361
</div>
13471362
<div className="description">{commit.message}</div>
13481363
{expanded && diffFiles && <DiffView parent={parent} blocksMode={false} diffFiles={diffFiles} cacheKey={commit.sha} />}
@@ -1354,6 +1369,7 @@ class CommitView extends sui.UIElement<CommitViewProps, CommitViewState> {
13541369
interface HistoryState {
13551370
expanded?: boolean;
13561371
selectedCommit?: pxt.github.CommitInfo;
1372+
selectedDay?: string;
13571373
}
13581374

13591375
class HistoryZone extends sui.UIElement<GitHubViewProps, HistoryState> {
@@ -1365,17 +1381,27 @@ class HistoryZone extends sui.UIElement<GitHubViewProps, HistoryState> {
13651381
handleLoadClick() {
13661382
pxt.tickEvent("github.history.load", undefined, { interactiveConsent: true });
13671383
const { expanded } = this.state;
1368-
this.setState({ expanded: !expanded, selectedCommit: undefined })
1384+
this.setState({ expanded: !expanded, selectedCommit: undefined, selectedDay: undefined })
13691385
}
13701386

13711387
renderCore() {
13721388
const { githubId, gs, parent } = this.props;
1373-
const { selectedCommit, expanded } = this.state;
1389+
const { selectedCommit, expanded, selectedDay } = this.state;
13741390
const inverted = !!pxt.appTarget.appTheme.invertedGitHub;
13751391
const commits = expanded &&
1376-
this.getData(`gh-commits:${gs.repo}#${gs.commit.sha}`) as pxt.github.CommitInfo[];
1392+
this.getData(`gh-commits:${githubId.fullName}#${gs.commit.sha}`) as pxt.github.CommitInfo[];
13771393
const loading = expanded && !commits;
13781394

1395+
// group commits by day
1396+
const days: pxt.Map<pxt.github.CommitInfo[]> = {};
1397+
if (commits)
1398+
commits.forEach(commit => {
1399+
const day = new Date(Date.parse(commit.author.date)).toLocaleDateString();
1400+
let dcommit = days[day];
1401+
if (!dcommit) dcommit = days[day] = [];
1402+
dcommit.push(commit);
1403+
})
1404+
13791405
return <div className={`ui transparent ${inverted ? 'inverted' : ''} segment`}>
13801406
<div className="ui header">{lf("History")}</div>
13811407
{(loading || !expanded) && <div className="ui field">
@@ -1388,19 +1414,40 @@ class HistoryZone extends sui.UIElement<GitHubViewProps, HistoryState> {
13881414
{sui.helpIconLink("/github/history", lf("Learn more about history of commits."))}
13891415
</span>
13901416
</div>}
1391-
{commits && <div className="ui divided items">
1392-
{commits.map(commit => <CommitView
1393-
key={'commit' + commit.sha}
1394-
onClick={() => {
1395-
pxt.tickEvent("github.history.selectcommit", undefined, { interactiveConsent: true })
1396-
const { selectedCommit } = this.state;
1397-
this.setState({ selectedCommit: commit == selectedCommit ? undefined : commit })
1398-
}}
1399-
commit={commit}
1400-
parent={parent}
1401-
githubId={githubId}
1402-
expanded={selectedCommit === commit}
1403-
/>)}
1417+
{commits && <div className="ui items">
1418+
{Object.keys(days).map(day =>
1419+
<div role="button" className="ui link item"
1420+
key={"commitday" + day}
1421+
onClick={e => {
1422+
e.stopPropagation();
1423+
pxt.tickEvent("github.history.selectday");
1424+
this.setState({ selectedDay: selectedDay === day ? undefined : day, selectedCommit: undefined });
1425+
}}
1426+
onKeyDown={sui.fireClickOnEnter}>
1427+
<div className="content">
1428+
<div className="ui header">{day}
1429+
<div className="ui label">
1430+
<i className="long arrow alternate up icon"></i> {days[day].length}
1431+
</div>
1432+
</div>
1433+
{day === selectedDay &&
1434+
<div className="ui divided items">
1435+
{days[day].map(commit => <CommitView
1436+
key={'commit' + commit.sha}
1437+
onClick={e => {
1438+
e.stopPropagation();
1439+
pxt.tickEvent("github.history.selectcommit", undefined, { interactiveConsent: true })
1440+
const { selectedCommit } = this.state;
1441+
this.setState({ selectedCommit: commit == selectedCommit ? undefined : commit })
1442+
}}
1443+
commit={commit}
1444+
parent={parent}
1445+
githubId={githubId}
1446+
expanded={selectedCommit === commit}
1447+
/>)}
1448+
</div>}
1449+
</div>
1450+
</div>)}
14041451
</div>}
14051452
</div>
14061453
}

0 commit comments

Comments
 (0)