diff --git a/.github/workflows/build-development.yml b/.github/workflows/build-development.yml
index 482b8b146c..b51542b7b2 100644
--- a/.github/workflows/build-development.yml
+++ b/.github/workflows/build-development.yml
@@ -1,4 +1,4 @@
-name: Build source-academy.github.io
+name: Build sourceacademy.org
on:
push:
branches:
@@ -6,7 +6,7 @@ on:
jobs:
deploy:
- name: Deploy to GitHub Pages
+ name: Build and deploy sourceacademy.org
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
@@ -14,6 +14,9 @@ jobs:
uses: actions/setup-node@v2-beta
with:
node-version: '14'
+ - name: Setup Sentry CLI
+ run: |
+ curl -sL https://sentry.io/get-cli/ | INSTALL_DIR=. bash
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -50,28 +53,31 @@ jobs:
REACT_APP_ENVIRONMENT: "pages"
REACT_APP_MODULE_BACKEND_URL: https://source-academy.github.io/modules
REACT_APP_SHAREDB_BACKEND_URL: ${{ secrets.REACT_APP_SHAREDB_BACKEND_URL }}
- REACT_APP_SW_EXCLUDE_REGEXES: '["^/source", "^/sicp", "^/modules", "^/ev3-source"]'
- PUBLIC_URL: "https://source-academy.github.io"
+ PUBLIC_URL: "https://sourceacademy.org"
REACT_APP_GITHUB_OAUTH_PROXY_URL: ${{ secrets.REACT_APP_GITHUB_OAUTH_PROXY_URL }}
REACT_APP_GITHUB_CLIENT_ID: ${{ secrets.REACT_APP_GITHUB_CLIENT_ID }}
- - name: Create symbolic links
+ - name: Create Sentry release
+ working-directory: build
+ run: |
+ SENTRY_RELEASE="cadet-frontend@$REACT_APP_VERSION"
+ "$GITHUB_WORKSPACE/sentry-cli" releases new -p "$SENTRY_PROJECT" "$SENTRY_RELEASE"
+ "$GITHUB_WORKSPACE/sentry-cli" releases set-commits --auto "$SENTRY_RELEASE"
+ "$GITHUB_WORKSPACE/sentry-cli" releases files "$SENTRY_RELEASE" upload-sourcemaps --url-prefix '~/static/js' --rewrite static/js
+ "$GITHUB_WORKSPACE/sentry-cli" releases finalize "$SENTRY_RELEASE"
+ "$GITHUB_WORKSPACE/sentry-cli" releases deploys "$SENTRY_RELEASE" new -e pages
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: sourceacademy
+ SENTRY_PROJECT: cadet-frontend
+ REACT_APP_VERSION: ${{ format('{0}-{1}', github.sha, steps.get-time.outputs.time) }}
+ - name: Remove sourcemaps
+ working-directory: build
run: |
- set -euxo pipefail
- ln -s index.html build/playground.html
- ln -s index.html build/contributors.html
- ln -s index.html build/sourcecast.html
- ln -s index.html build/interactive-sicp.html
- mkdir -p build/interactive-sicp
- declare -a arr=("index" "foreword" "prefaces03" "prefaces84" "prefaces96" "acknowledgements" "1" "1.1" "1.1.1" "1.1.2" "1.1.3" "1.1.4" "1.1.5" "1.1.6" "1.1.7" "1.1.8" "1.2" "1.2.1" "1.2.2" "1.2.3" "1.2.4" "1.2.5" "1.2.6" "1.3" "1.3.1" "1.3.2" "1.3.3" "1.3.4" "2" "2.1" "2.1.1" "2.1.2" "2.1.3" "2.1.4" "2.2" "2.2.1" "2.2.2" "2.2.3" "2.2.4" "2.3" "2.3.1" "2.3.2" "2.3.3" "2.3.4" "2.4" "2.4.1" "2.4.2" "2.4.3" "2.5" "2.5.1" "2.5.2" "2.5.3" "3" "3.1" "3.1.1" "3.1.2" "3.1.3" "3.2" "3.2.1" "3.2.2" "3.2.3" "3.2.4" "3.3" "3.3.1" "3.3.2" "3.3.3" "3.3.4" "3.3.5" "3.4" "3.4.1" "3.4.2" "3.5" "3.5.1" "3.5.2" "3.5.3" "3.5.4" "3.5.5" "4" "4.1" "4.1.1" "4.1.2" "4.1.3" "4.1.4" "4.1.5" "4.1.6" "4.1.7" "4.2" "4.2.1" "4.2.2" "4.2.3" "4.3" "4.3.1" "4.3.2" "4.3.3" "4.4" "4.4.1" "4.4.2" "4.4.3" "4.4.4" "5" "5.1" "5.1.1" "5.1.2" "5.1.3" "5.1.4" "5.1.5" "5.2" "5.2.1" "5.2.2" "5.2.3" "5.2.4" "5.3" "5.3.1" "5.3.2" "5.4" "5.4.1" "5.4.2" "5.4.3" "5.4.4" "5.5" "5.5.1" "5.5.2" "5.5.3" "5.5.4" "5.5.5" "5.5.6" "5.5.7" "references" "making-of")
- for i in "${arr[@]}"; do
- ln -s ../index.html build/interactive-sicp/$i.html
- done
- # the `ln`s above are a hack to make GitHub Pages route /playground
- # and /contributors etc to index, instead of 404-ing.
+ find -name '*.map' -print -delete
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
- external_repository: source-academy/source-academy.github.io
+ external_repository: source-academy/sourceacademy.org
deploy_key: ${{ secrets.DEPLOY_PRIVATE_KEY }}
publish_dir: ./build
publish_branch: master
diff --git a/README.md b/README.md
index f083bd3218..fd04fc7481 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,10 @@
[![Coverage Status](https://coveralls.io/repos/github/source-academy/cadet-frontend/badge.svg?branch=master)](https://coveralls.io/github/source-academy/cadet-frontend?branch=master)
[![License](https://img.shields.io/github/license/source-academy/cadet-frontend)](https://github.com/source-academy/cadet-frontend/blob/master/LICENSE)
-The Source Academy ( ) is an immersive online experiential environment for learning programming. It is developed by a community of learners (also called "Source Academy") who use the book [Structure and Interpretation of Computer Programs, JavaScript Adaptation](https://source-academy.github.io/interactive-sicp) (SICP JS). This repository houses the sources for the frontend of the Source Academy, written in ReactJS with Redux.
+The Source Academy ( ) is an immersive online experiential environment for learning programming. It is developed by a community of learners (also called "Source Academy") who use the book [Structure and Interpretation of Computer Programs, JavaScript Adaptation](https://sourceacademy.org/sicpjs) (SICP JS). This repository houses the sources for the frontend of the Source Academy, written in ReactJS with Redux.
## Features
+
- Playground to write and test programs
- Built-in Debugger and Visualizer to interact with your programs
- Missions/Quests/Contests to solve challenging problems while learning about programming fundamentals
diff --git a/public/externalLibs/inspector/inspector.js b/public/externalLibs/inspector/inspector.js
index 51b651f0d6..40bcd7017c 100644
--- a/public/externalLibs/inspector/inspector.js
+++ b/public/externalLibs/inspector/inspector.js
@@ -1,225 +1,4 @@
(function (exports) {
- var container = document.createElement('div');
- container.id = 'inspector-container';
-
- const builtins = [
- 'Infinity',
- 'NaN',
- 'accumulate',
- 'alert',
- 'append',
- 'apply_in_underlying_javascript',
- 'array_length',
- 'assoc',
- 'black',
- 'blue',
- 'brown',
- 'build_list',
- 'build_stream',
- 'color',
- 'display',
- 'display_list',
- 'draw_data',
- 'enum_list',
- 'enum_stream',
- 'equal',
- 'error',
- 'eval_stream',
- 'filter',
- 'for_each',
- 'get_time',
- 'green',
- 'has_own_property',
- 'head',
- 'indigo',
- 'integers_from',
- 'is',
- 'is_NaN',
- 'is_array',
- 'is_boolean',
- 'is_function',
- 'is_list',
- 'is_null',
- 'is_number',
- 'is_object',
- 'is_pair',
- 'is_stream',
- 'is_string',
- 'is_undefined',
- 'length',
- 'list',
- 'list_ref',
- 'list_to_stream',
- 'list_to_string',
- 'map',
- 'math_E',
- 'math_LN10',
- 'math_LN2',
- 'math_LOG10E',
- 'math_LOG2E',
- 'math_PI',
- 'math_SQRT1_2',
- 'math_SQRT2',
- 'math_abs',
- 'math_acos',
- 'math_acosh',
- 'math_asin',
- 'math_asinh',
- 'math_atan',
- 'math_atan2',
- 'math_atanh',
- 'math_cbrt',
- 'math_ceil',
- 'math_clz32',
- 'math_cos',
- 'math_cosh',
- 'math_exp',
- 'math_expm1',
- 'math_floor',
- 'math_fround',
- 'math_hypot',
- 'math_imul',
- 'math_log',
- 'math_log10',
- 'math_log1p',
- 'math_log2',
- 'math_max',
- 'math_min',
- 'math_pow',
- 'math_random',
- 'math_round',
- 'math_sign',
- 'math_sin',
- 'math_sinh',
- 'math_sqrt',
- 'math_tan',
- 'math_tanh',
- 'math_toSource',
- 'math_trunc',
- 'member',
- 'orange',
- 'pair',
- 'parse',
- 'parse_int',
- 'pink',
- 'prompt',
- 'purple',
- 'quarter_turn_left',
- 'quarter_turn_right',
- 'random_color',
- 'raw_display',
- 'red',
- 'remove',
- 'remove_all',
- 'reverse',
- 'rotate',
- 'runtime',
- 'scale',
- 'scale_independent',
- 'set_head',
- 'set_tail',
- 'show',
- 'stack',
- 'stack_frac',
- 'stackn',
- 'stream',
- 'stream_append',
- 'stream_filter',
- 'stream_for_each',
- 'stream_length',
- 'stream_map',
- 'stream_member',
- 'stream_ref',
- 'stream_remove',
- 'stream_remove_all',
- 'stream_reverse',
- 'stream_tail',
- 'stream_to_list',
- 'stringify',
- 'tail',
- 'translate',
- 'undefined',
- 'white',
- 'yellow'
- ];
-
- function filter(str) {
- // regex to match: replacement for match
- swapTable = {
- programEnvironment: 'Program',
- forLoopEnvironment: 'Body of for
',
- forBlockEnvironment: 'Control statement of for
',
- blockEnvironment: 'Block',
- '[.]* => [.]*': ' => ',
- '{[\\s\\S]*}': '{...}',
- 'Symbol.*': '-'
- };
- for (var r in swapTable) {
- str = str.replace(new RegExp(r), swapTable[r]);
- }
- return str;
- }
-
- function updateContext(context, stringify) {
- // Hides the default text
- const defaultText = document.getElementById('inspector-default-text');
- if (defaultText) {
- defaultText.hidden = true;
- }
-
- function dumpTable(env) {
- var res = '';
- for (var k in env) {
- if (builtins.indexOf('' + k) < 0) {
- var str = filter(stringify(env[k]));
- res +=
- '
' +
- k +
- ' ' +
- '' +
- filter(str) +
- '
';
- }
- }
- return res.length > 0 ? res : undefined;
- }
-
- function drawOutput() {
- var frames = context.context.runtime.environments;
- container.innerHTML = '';
- for (var frame of frames) {
- var envtoString = dumpTable(frame.head);
- if (envtoString == undefined) {
- // skipping either empty frame or perhaps the global
- continue;
- }
- var newtable = document.createElement('table');
- newtable.classList.add('inspect-scope-table');
- newtable.innerHTML = ' ';
- var tbody = document.createElement('tbody');
- tbody.innerHTML =
- '' + filter(frame.name) + ' ' + envtoString;
- newtable.appendChild(tbody);
- container.appendChild(newtable);
- }
- }
-
- // icon to blink
- const icon = document.getElementById('inspector-icon');
-
- if (context) {
- drawOutput();
- if (icon) {
- icon.classList.add('side-content-tab-alert'); // this blinks the icon
- }
- } else if (icon) {
- // here we have no context! don't alert the inspector...
- document.getElementById('inspector-default-text').hidden = false;
- icon.classList.remove('side-content-tab-alert');
- container.innerHTML = '';
- }
- }
-
function highlightClean() {
var gutterCells = document.getElementsByClassName('ace_gutter-cell');
var aceLines = document.getElementsByClassName('ace_line');
@@ -241,12 +20,6 @@
}
exports.Inspector = {
- builtins,
- filter,
- init: function (parent) {
- parent.appendChild(container);
- },
- updateContext,
highlightLine,
highlightClean
};
diff --git a/src/commons/__tests__/__snapshots__/Markdown.tsx.snap b/src/commons/__tests__/__snapshots__/Markdown.tsx.snap
index 7dc4c37d2f..94b2f32f71 100644
--- a/src/commons/__tests__/__snapshots__/Markdown.tsx.snap
+++ b/src/commons/__tests__/__snapshots__/Markdown.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown page renders correctly 1`] = `
-"
+"
"
`;
diff --git a/src/commons/application/Application.tsx b/src/commons/application/Application.tsx
index 5e8e52cf00..f3fdd74e06 100644
--- a/src/commons/application/Application.tsx
+++ b/src/commons/application/Application.tsx
@@ -6,8 +6,7 @@ import Academy from '../../pages/academy/AcademyContainer';
import Achievement from '../../pages/achievement/AchievementContainer';
import Contributors from '../../pages/contributors/Contributors';
import Disabled from '../../pages/disabled/Disabled';
-import GitHubAssessmentWorkspaceContainer from '../../pages/githubAssessments/GitHubAssessmentWorkspaceContainer';
-import GitHubMissionListing from '../../pages/githubAssessments/GitHubMissionListing';
+import GitHubClassroom from '../../pages/githubAssessments/GitHubClassroom';
import GitHubCallback from '../../pages/githubCallback/GitHubCallback';
import Login from '../../pages/login/LoginContainer';
import MissionControlContainer from '../../pages/missionControl/MissionControlContainer';
@@ -149,24 +148,19 @@ const Application: React.FC = props => {
{Constants.enableGitHubAssessments && (
(
-
)}
/>
)}
- {Constants.enableGitHubAssessments && (
-
- )}
-
-
+
+
+
{fullPaths}
= props => {
const redirectToPlayground = () => ;
const redirectToAcademy = () => ;
const redirectToLogin = () => ;
-const redirectToSicp = () => ;
+const redirectToSicp = () => ;
/**
* A user routes to /academy,
diff --git a/src/commons/application/__tests__/__snapshots__/Application.tsx.snap b/src/commons/application/__tests__/__snapshots__/Application.tsx.snap
index 6f5e6060cf..822ba8dbd6 100644
--- a/src/commons/application/__tests__/__snapshots__/Application.tsx.snap
+++ b/src/commons/application/__tests__/__snapshots__/Application.tsx.snap
@@ -8,11 +8,11 @@ exports[`Application renders correctly 1`] = `
-
-
+
-
-
+
+
+
diff --git a/src/commons/controlBar/ControlBarShareButton.tsx b/src/commons/controlBar/ControlBarShareButton.tsx
index b1c1db0a11..b7afc3b596 100644
--- a/src/commons/controlBar/ControlBarShareButton.tsx
+++ b/src/commons/controlBar/ControlBarShareButton.tsx
@@ -1,4 +1,4 @@
-import { NonIdealState, Position, Spinner, Text } from '@blueprintjs/core';
+import { NonIdealState, Position, Spinner, SpinnerSize, Text } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2, Tooltip2 } from '@blueprintjs/popover2';
import * as React from 'react';
@@ -19,6 +19,7 @@ type StateProps = {
queryString?: string;
shortURL?: string;
key: string;
+ isSicp?: boolean;
};
type State = {
@@ -51,6 +52,15 @@ export class ControlBarShareButton extends React.PureComponent
+ ) : this.props.isSicp ? (
+
+
+
+
+ {controlButton('', IconNames.DUPLICATE, this.selectShareInputText)}
+
+
+
) : (
<>
{!this.props.shortURL || this.props.shortURL === 'ERROR' ? (
@@ -71,7 +81,7 @@ export class ControlBarShareButton extends React.PureComponent
}
+ icon={ }
/>
)
diff --git a/src/commons/controlBar/github/ControlBarGitHubLoginButton.tsx b/src/commons/controlBar/github/ControlBarGitHubLoginButton.tsx
index f0e1750cc7..f521ffe3f6 100644
--- a/src/commons/controlBar/github/ControlBarGitHubLoginButton.tsx
+++ b/src/commons/controlBar/github/ControlBarGitHubLoginButton.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useSelector } from 'react-redux';
import { useMediaQuery } from 'react-responsive';
+import { OverallState } from '../../application/ApplicationTypes';
import controlButton from '../../ControlButton';
import Constants from '../../utils/Constants';
@@ -20,7 +21,7 @@ export type ControlBarGitHubLoginButtonProps = {
export const ControlBarGitHubLoginButton: React.FC = props => {
const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
const isLoggedIn =
- useSelector((store: any) => store.session.githubOctokitObject).octokit !== undefined;
+ useSelector((store: OverallState) => store.session.githubOctokitObject).octokit !== undefined;
const loginButton = isLoggedIn
? controlButton('Log Out', IconNames.GIT_BRANCH, props.onClickLogOut)
diff --git a/src/commons/gitHubOverlay/FileExplorerDialog.tsx b/src/commons/gitHubOverlay/FileExplorerDialog.tsx
index 8e2618d4a0..e72c7e7dd5 100644
--- a/src/commons/gitHubOverlay/FileExplorerDialog.tsx
+++ b/src/commons/gitHubOverlay/FileExplorerDialog.tsx
@@ -147,6 +147,7 @@ const FileExplorerDialog: React.FC = props => {
}
}
}
+ props.onSubmit('');
}
async function handleNodeClick(
diff --git a/src/commons/githubAssessments/GitHubMissionDataUtils.ts b/src/commons/githubAssessments/GitHubMissionDataUtils.ts
index 9968b8ac81..7c0a3c04b0 100644
--- a/src/commons/githubAssessments/GitHubMissionDataUtils.ts
+++ b/src/commons/githubAssessments/GitHubMissionDataUtils.ts
@@ -265,91 +265,18 @@ export async function getContentAsString(
* @param metadataString The file contents of the '.metadata' file of a mission repository
*/
function convertMetadataStringToMissionMetadata(metadataString: string) {
- const missionMetadata: MissionMetadata = {
- coverImage: '',
- type: '',
- id: '',
- title: '',
- sourceVersion: 1,
- dueDate: new Date(8640000000000000),
- reading: '',
- webSummary: ''
- };
- const stringPropsToExtract = ['coverImage', 'type', 'id', 'title', 'reading', 'webSummary'];
- const numPropsToExtract = ['sourceVersion'];
- const datePropsToExtract = ['dueDate'];
-
- const retVal = parseMetadataProperties(
- missionMetadata,
- stringPropsToExtract,
- numPropsToExtract,
- datePropsToExtract,
- metadataString
- );
-
- return retVal;
+ try {
+ return JSON.parse(metadataString) as MissionMetadata;
+ } catch (err) {
+ console.error(err);
+ return {
+ sourceVersion: 4
+ } as MissionMetadata;
+ }
}
function convertMissionMetadataToMetadataString(missionMetadata: MissionMetadata) {
- const properties: string[] = [
- 'title',
- 'coverImage',
- 'webSummary',
- 'dueDate',
- 'type',
- 'id',
- 'sourceVersion',
- 'reading'
- ];
- const propertyValuePairs = properties.map(property => property + '=' + missionMetadata[property]);
- return propertyValuePairs.join('\n');
-}
-
-/**
- * Converts the contents of a '.metadata' file into an object of type R.
- *
- * @param propertyContainer The object of which properties will be set
- * @param stringProps An array containing the names of properties with string values
- * @param numProps An array containing the names of properties with numerical values
- * @param dateProps An array containing the names of properties with date values
- * @param metadataString The content of the '.metadata' file to be parsed
- */
-export function parseMetadataProperties(
- propertyContainer: R,
- stringProps: string[],
- numProps: string[],
- dateProps: string[],
- metadataString: string
-) {
- const lines = metadataString.replace(/\r/g, '').split(/\n/);
-
- lines.forEach(line => {
- for (let i = 0; i < stringProps.length; i++) {
- const propName = stringProps[i];
- if (line.startsWith(propName)) {
- propertyContainer[propName] = line.substr(propName.length + 1);
- return;
- }
- }
-
- for (let i = 0; i < numProps.length; i++) {
- const propName = numProps[i];
- if (line.startsWith(propName)) {
- propertyContainer[propName] = parseInt(line.substr(propName.length + 1), 10);
- return;
- }
- }
-
- for (let i = 0; i < dateProps.length; i++) {
- const propName = dateProps[i];
- if (line.startsWith(propName)) {
- propertyContainer[propName] = new Date(line.substr(propName.length + 1));
- return;
- }
- }
- });
-
- return propertyContainer;
+ return jsonStringify(missionMetadata);
}
/**
diff --git a/src/commons/githubAssessments/GitHubMissionMobileLoginButton.tsx b/src/commons/githubAssessments/GitHubMissionMobileLoginButton.tsx
index b4c670540a..df372f7044 100644
--- a/src/commons/githubAssessments/GitHubMissionMobileLoginButton.tsx
+++ b/src/commons/githubAssessments/GitHubMissionMobileLoginButton.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useSelector } from 'react-redux';
import { useMediaQuery } from 'react-responsive';
+import { OverallState } from '../application/ApplicationTypes';
import controlButton from '../ControlButton';
import Constants from '../utils/Constants';
@@ -21,7 +22,7 @@ export const ControlBarGitHubMobileLoginButton: React.FC {
const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
const isLoggedIn =
- useSelector((store: any) => store.session.githubOctokitObject).octokit !== undefined;
+ useSelector((store: OverallState) => store.session.githubOctokitObject).octokit !== undefined;
const loginButton = isLoggedIn
? controlButton('Log Out', IconNames.GIT_BRANCH, props.onClickLogOut)
diff --git a/src/commons/githubAssessments/GitHubMissionTypes.ts b/src/commons/githubAssessments/GitHubMissionTypes.ts
index 528768de5c..0eb75b6d96 100644
--- a/src/commons/githubAssessments/GitHubMissionTypes.ts
+++ b/src/commons/githubAssessments/GitHubMissionTypes.ts
@@ -17,15 +17,7 @@ export type TaskData = {
* An code representation of a GitHub-hosted mission's '.metadata' file.
*/
export type MissionMetadata = {
- coverImage: string;
- type: string;
- id: string;
- title: string;
sourceVersion: number;
- dueDate: Date;
-
- reading: string;
- webSummary: string;
};
/**
diff --git a/src/commons/githubAssessments/__tests__/GitHubMissionDataUtils.ts b/src/commons/githubAssessments/__tests__/GitHubMissionDataUtils.ts
index 1e4a5de529..d085cd8b45 100644
--- a/src/commons/githubAssessments/__tests__/GitHubMissionDataUtils.ts
+++ b/src/commons/githubAssessments/__tests__/GitHubMissionDataUtils.ts
@@ -2,7 +2,7 @@ import { Octokit } from '@octokit/rest';
import { IMCQQuestion } from '../../assessment/AssessmentTypes';
import * as GitHubMissionDataUtils from '../GitHubMissionDataUtils';
-import { MissionMetadata, MissionRepoData } from '../GitHubMissionTypes';
+import { MissionRepoData } from '../GitHubMissionTypes';
test('getContentAsString correctly gets content and translates from Base64 to utf-8', async () => {
const octokit = new Octokit();
@@ -23,40 +23,6 @@ test('getContentAsString correctly gets content and translates from Base64 to ut
expect(content).toBe('Hello World!');
});
-test('parseMetadataProperties correctly discovers properties', () => {
- const missionMetadata = Object.assign({}, dummyMissionMetadata);
- const stringPropsToExtract = ['coverImage', 'type', 'id', 'title', 'reading', 'webSummary'];
- const numPropsToExtract = ['sourceVersion'];
- const datePropsToExtract = ['dueDate'];
-
- const metadataString =
- 'coverImage=www.somelink.com\n' +
- 'type=Mission\n' +
- 'id=M3\n' +
- 'title=Dummy Mission\n' +
- 'reading=Textbook Pages 1 to 234763\n' +
- 'dueDate=December 17, 1995 03:24:00\n' +
- 'webSummary=no\n' +
- 'sourceVersion=3';
-
- const retVal = GitHubMissionDataUtils.parseMetadataProperties(
- missionMetadata,
- stringPropsToExtract,
- numPropsToExtract,
- datePropsToExtract,
- metadataString
- );
-
- expect(retVal.coverImage).toBe('www.somelink.com');
- expect(retVal.type).toBe('Mission');
- expect(retVal.id).toBe('M3');
- expect(retVal.title).toBe('Dummy Mission');
- expect(retVal.reading).toBe('Textbook Pages 1 to 234763');
- expect(retVal.webSummary).toBe('no');
- expect(retVal.sourceVersion).toBe(3);
- expect(retVal.dueDate).toStrictEqual(new Date('December 17, 1995 03:24:00'));
-});
-
test('getMissionData works properly', async () => {
const missionRepoData: MissionRepoData = {
repoOwner: 'Pain',
@@ -79,13 +45,9 @@ test('getMissionData works properly', async () => {
// Metadata String
const contentResponse = generateGetContentResponse();
(contentResponse.data as any).content = Buffer.from(
- 'coverImage=www.somelink.com\n' +
- 'type=Mission\n' +
- 'id=M3\n' +
- 'title=Dummy Mission\n' +
- 'reading=Textbook Pages 1 to 234763\n' +
- 'webSummary=no\n' +
- 'sourceVersion=3',
+ `{
+ "sourceVersion": 3
+ }`,
'utf-8'
).toString('base64');
return contentResponse;
@@ -183,13 +145,6 @@ test('getMissionData works properly', async () => {
expect(missionData.missionRepoData.repoName).toBe('Peko');
expect(missionData.missionBriefing).toBe('Briefing Content');
-
- expect(missionData.missionMetadata.coverImage).toBe('www.somelink.com');
- expect(missionData.missionMetadata.type).toBe('Mission');
- expect(missionData.missionMetadata.id).toBe('M3');
- expect(missionData.missionMetadata.title).toBe('Dummy Mission');
- expect(missionData.missionMetadata.reading).toBe('Textbook Pages 1 to 234763');
- expect(missionData.missionMetadata.webSummary).toBe('no');
expect(missionData.missionMetadata.sourceVersion).toBe(3);
expect(missionData.tasksData.length).toBe(2);
@@ -932,14 +887,7 @@ function generateGetContentResponse() {
}
const dummyMissionMetadata = {
- coverImage: 'www.eh',
- type: 'mission',
- id: 'M2',
- title: 'Dummy',
- sourceVersion: 1,
- dueDate: new Date('December 17, 1996 03:24:00'),
- reading: 'none',
- webSummary: 'no'
+ sourceVersion: 1
};
const defaultMissionMetadata = {
diff --git a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx
index 5921d91edc..292e6553bc 100644
--- a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx
+++ b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx
@@ -89,7 +89,7 @@ const MobileSideContent: React.FC = props =>
* would force React.useMemo to recompute the nullary function anyway
*/
const renderedPanels = () => {
- // TODO: Fix the CSS of all the panels (e.g. subst_visualizer, inspector, etc.)
+ // TODO: Fix the CSS of all the panels (e.g. subst_visualizer)
const renderPanel = (tab: SideContentTab, workspaceLocation?: WorkspaceLocation) => {
const tabBody: JSX.Element = workspaceLocation
? {
@@ -175,7 +175,7 @@ const MobileSideContent: React.FC = props =>
/**
* Remove the 'side-content-tab-alert' class that causes tabs flash.
* To be run when tabs are changed.
- * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs.
+ * Currently this style is only used for the "Env Visualizer" tab.
*/
const resetAlert = (prevTabId: TabId) => {
const iconId = generateIconId(prevTabId);
diff --git a/src/commons/navigationBar/NavigationBar.tsx b/src/commons/navigationBar/NavigationBar.tsx
index 72e4de132d..edd3afa15e 100644
--- a/src/commons/navigationBar/NavigationBar.tsx
+++ b/src/commons/navigationBar/NavigationBar.tsx
@@ -17,13 +17,12 @@ import classNames from 'classnames';
import * as React from 'react';
import { useMediaQuery } from 'react-responsive';
import { NavLink, Route, Switch } from 'react-router-dom';
-import SicpNavigationBar from 'src/commons/navigationBar/subcomponents/SicpNavigationBar';
+import SicpNavigationBar from '../../commons/navigationBar/subcomponents/SicpNavigationBar';
import { Role } from '../application/ApplicationTypes';
import Dropdown from '../dropdown/Dropdown';
import Constants from '../utils/Constants';
import AcademyNavigationBar from './subcomponents/AcademyNavigationBar';
-import GitHubAssessmentsNavigationBar from './subcomponents/GitHubAssessmentsNavigationBar';
import NavigationBarMobileSideMenu from './subcomponents/NavigationBarMobileSideMenu';
type NavigationBarProps = DispatchProps & StateProps;
@@ -97,7 +96,7 @@ const NavigationBar: React.FC = props => {
onClick={() => setMobileSideMenuOpen(false)}
>
- GitHub Assessments
+ Classroom
= props => {
Classes.MINIMAL,
Classes.LARGE
)}
- to="/interactive-sicp/index"
+ to="/sicpjs/index"
onClick={() => setMobileSideMenuOpen(false)}
>
@@ -124,20 +123,20 @@ const NavigationBar: React.FC = props => {
to="/playground"
>
- Source Academy Playground
+ Playground
- GitHub Assessments
+ Classroom
SICP JS
@@ -152,12 +151,12 @@ const NavigationBar: React.FC = props => {
to="/playground"
>
- Source Academy Playground
+ Playground
SICP JS
@@ -225,17 +224,17 @@ const NavigationBar: React.FC = props => {
- GitHub Assessments
+ Classroom
)}
SICP JS
@@ -302,12 +301,7 @@ const NavigationBar: React.FC = props => {
-
- {Constants.enableGitHubAssessments && !isMobileBreakpoint && desktopMenuOpen && (
-
- )}
-
-
+
diff --git a/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap
index 7408135b02..6196e451eb 100644
--- a/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap
+++ b/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap
@@ -22,13 +22,13 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = `
Playground
-
+
- GitHub Assessments
+ Classroom
-
+
SICP JS
@@ -52,10 +52,7 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = `
-
-
-
-
+
@@ -85,13 +82,13 @@ exports[`NavigationBar renders correctly with student role 1`] = `
Playground
-
+
- GitHub Assessments
+ Classroom
-
+
SICP JS
@@ -125,10 +122,7 @@ exports[`NavigationBar renders correctly with student role 1`] = `
-
-
-
-
+
diff --git a/src/commons/navigationBar/subcomponents/GitHubAssessmentsNavigationBar.tsx b/src/commons/navigationBar/subcomponents/GitHubAssessmentsNavigationBar.tsx
index 2953cf2dda..2017300340 100644
--- a/src/commons/navigationBar/subcomponents/GitHubAssessmentsNavigationBar.tsx
+++ b/src/commons/navigationBar/subcomponents/GitHubAssessmentsNavigationBar.tsx
@@ -1,42 +1,113 @@
-import { Alignment, Classes, Icon, Navbar, NavbarGroup } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
+import {
+ Alignment,
+ Button,
+ Classes,
+ Icon,
+ InputGroup,
+ Menu,
+ MenuItem,
+ Navbar,
+ NavbarGroup
+} from '@blueprintjs/core';
+import { IconName, IconNames } from '@blueprintjs/icons';
+import { Popover2 } from '@blueprintjs/popover2';
+import { Octokit } from '@octokit/rest';
import classNames from 'classnames';
import * as React from 'react';
import { NavLink } from 'react-router-dom';
+import { assessmentTypeLink } from '../../../commons/utils/ParamParseHelper';
+import { GHAssessmentTypeOverview } from '../../../pages/githubAssessments/GitHubClassroom';
import { ControlBarGitHubLoginButton } from '../../controlBar/github/ControlBarGitHubLoginButton';
-type OwnProps = {
- handleGitHubLogIn: any;
- handleGitHubLogOut: any;
+type GitHubAssessmentsNavigationBarProps = DispatchProps & StateProps;
+
+type DispatchProps = {
+ changeCourseHandler: (e: any) => void;
+ handleGitHubLogIn: () => void;
+ handleGitHubLogOut: () => void;
+};
+
+type StateProps = {
+ octokit?: Octokit;
+ courses?: string[];
+ selectedCourse: string;
+ types?: string[];
+ assessmentTypeOverviews?: GHAssessmentTypeOverview[];
};
/**
* The white navbar for the website. Should only be displayed when using GitHub-hosted missions.
- *
- * @param props Component properties
*/
-const GitHubAssessmentsNavigationBar: React.FunctionComponent = props => (
-
-
-
-
- Missions
-
-
-
-
-
-
-
-);
+const GitHubAssessmentsNavigationBar: React.FC = props => {
+ const handleClick = (e: any) => {
+ props.changeCourseHandler(e);
+ };
+
+ return (
+
+
+ {props.types?.map((type, idx) => {
+ return (
+
+
+ {type}
+
+ );
+ })}
+
+
+ {props.octokit !== undefined && props.types && props.types.length > 0 && (
+
+ {props.courses?.map((course: string) => (
+
+ ))}
+
+ }
+ placement={'bottom-end'}
+ >
+
+
+ }
+ placeholder={'Select Course'}
+ onChange={props.changeCourseHandler}
+ value={props.selectedCourse}
+ />
+ )}
+
+
+
+ );
+};
+
+const icons: IconName[] = [
+ IconNames.FLAME,
+ IconNames.LIGHTBULB,
+ IconNames.PREDICTIVE_ANALYSIS,
+ IconNames.COMPARISON,
+ IconNames.MANUAL
+];
export default GitHubAssessmentsNavigationBar;
diff --git a/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx b/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx
index ae09d10f9e..bedd1cd180 100644
--- a/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx
+++ b/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx
@@ -165,11 +165,11 @@ const NavigationBarMobileSideMenu: React.FC =
Classes.MINIMAL,
Classes.LARGE
)}
- to="/githubassessments/missions"
+ to="/githubassessments"
onClick={props.onClose}
>
- GitHub Assessments
+ Classroom
=
Classes.MINIMAL,
Classes.LARGE
)}
- to="/interactive-sicp/index"
+ to="/sicpjs/index"
onClick={props.onClose}
>
diff --git a/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx b/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx
index 33046fde69..f5922ef7ea 100644
--- a/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx
+++ b/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx
@@ -3,8 +3,8 @@ import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
import { useHistory, useParams } from 'react-router';
import controlButton from 'src/commons/ControlButton';
+import { getNext, getPrev } from 'src/features/sicp/TableOfContentsHelper';
-import tocNavigation from '../../../features/sicp/data/toc-navigation.json';
import { TableOfContentsButton } from '../../../features/sicp/TableOfContentsButton';
import SicpToc from '../../../pages/sicp/subcomponents/SicpToc';
@@ -16,59 +16,33 @@ const SicpNavigationBar: React.FC = () => {
const { section } = useParams<{ section: string }>();
const history = useHistory();
+ const prev = getPrev(section);
+ const next = getNext(section);
+
const handleCloseToc = () => setIsTocOpen(false);
+ const handleOpenToc = () => setIsTocOpen(true);
+ const handleNavigation = (sect: string) => {
+ history.push('/sicpjs/' + sect);
+ };
// Button to open table of contents
- const tocButton = React.useMemo(() => {
- const handleOpenToc = () => setIsTocOpen(true);
- return ;
- }, []);
+ const tocButton = ;
// Previous button only displayed when next page is valid.
- const prevButton = React.useMemo(() => {
- const sect = tocNavigation[section];
- if (!sect) {
- return;
- }
-
- const prev = sect['prev'];
- if (!prev) {
- return;
- }
-
- const handlePrev = () => {
- history.push('/interactive-sicp/' + prev);
- };
-
- return (
- prev && {controlButton('Previous', IconNames.ARROW_LEFT, handlePrev)}
- );
- }, [history, section]);
+ const prevButton = prev && (
+
+ {controlButton('Previous', IconNames.ARROW_LEFT, () => handleNavigation(prev))}
+
+ );
// Next button only displayed when next page is valid.
- const nextButton = React.useMemo(() => {
- const sect = tocNavigation[section];
- if (!sect) {
- return;
- }
-
- const next = sect['next'];
- if (!next) {
- return;
- }
-
- const handleNext = () => {
- history.push('/interactive-sicp/' + next);
- };
-
- return (
- next && (
-
- {controlButton('Next', IconNames.ARROW_RIGHT, handleNext, { iconOnRight: true })}
-
- )
- );
- }, [history, section]);
+ const nextButton = next && (
+
+ {controlButton('Next', IconNames.ARROW_RIGHT, () => handleNavigation(next), {
+ iconOnRight: true
+ })}
+
+ );
const drawerProps = {
onClose: handleCloseToc,
diff --git a/src/commons/navigationBar/subcomponents/__tests__/GitHubAssessmentsNavigationBar.tsx b/src/commons/navigationBar/subcomponents/__tests__/GitHubAssessmentsNavigationBar.tsx
index f10f83b442..68c8ab7cc8 100644
--- a/src/commons/navigationBar/subcomponents/__tests__/GitHubAssessmentsNavigationBar.tsx
+++ b/src/commons/navigationBar/subcomponents/__tests__/GitHubAssessmentsNavigationBar.tsx
@@ -1,10 +1,17 @@
+import { Octokit } from '@octokit/rest';
import { shallow } from 'enzyme';
import GitHubAssessmentsNavigationBar from '../GitHubAssessmentsNavigationBar';
const props = {
+ changeCourseHandler: () => {},
handleGitHubLogIn: () => {},
- handleGitHubLogOut: () => {}
+ handleGitHubLogOut: () => {},
+ octokit: new Octokit(undefined),
+ courses: [],
+ selectedCourse: '',
+ setSelectedCourse: () => {},
+ types: ['Missions', 'Quests']
};
test('Navbar renders correctly', () => {
diff --git a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/GitHubAssessmentsNavigationBar.tsx.snap b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/GitHubAssessmentsNavigationBar.tsx.snap
index edc85a7a71..7d6584197f 100644
--- a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/GitHubAssessmentsNavigationBar.tsx.snap
+++ b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/GitHubAssessmentsNavigationBar.tsx.snap
@@ -3,14 +3,21 @@
exports[`Navbar renders correctly 1`] = `
"
-
+
Missions
+
+
+
+ Quests
+
+
+
"
diff --git a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap
index b456c1c3eb..3d7291718f 100644
--- a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap
+++ b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap
@@ -14,13 +14,13 @@ exports[`NavigationBarMobileSideMenu renders "Not logged in" correctly 1`] = `
Playground
-
+
- GitHub Assessments
+ Classroom
-
+
SICP JS
@@ -77,13 +77,13 @@ exports[`NavigationBarMobileSideMenu renders correctly with student role 1`] = `
Playground
-
+
- GitHub Assessments
+ Classroom
-
+
SICP JS
diff --git a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/SicpNavigationBar.tsx.snap b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/SicpNavigationBar.tsx.snap
index 2f460b98d6..4a36f17df5 100644
--- a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/SicpNavigationBar.tsx.snap
+++ b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/SicpNavigationBar.tsx.snap
@@ -8,7 +8,7 @@ exports[`Navbar renders correctly 1`] = `
-
+
Next
diff --git a/src/commons/sagas/GitHubPersistenceSaga.ts b/src/commons/sagas/GitHubPersistenceSaga.ts
index 95100f763e..641e2bbb6e 100644
--- a/src/commons/sagas/GitHubPersistenceSaga.ts
+++ b/src/commons/sagas/GitHubPersistenceSaga.ts
@@ -19,7 +19,6 @@ import RepositoryDialog, { RepositoryDialogProps } from '../gitHubOverlay/Reposi
import { actions } from '../utils/ActionsHelper';
import Constants from '../utils/Constants';
import { promisifyDialog } from '../utils/DialogHelper';
-import { history } from '../utils/HistoryHelper';
import { showSuccessMessage, showWarningMessage } from '../utils/NotificationsHelper';
export function* GitHubPersistenceSaga(): SagaIterator {
@@ -64,7 +63,6 @@ function* githubLogoutSaga() {
yield put(actions.removeGitHubOctokitObjectAndAccessToken());
yield call(showSuccessMessage, `Logged out from GitHub`, 1000);
- yield call(history.push, '/githubassessments/missions');
}
function* githubOpenFile(): any {
diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts
index b262959aca..e7372b19cc 100644
--- a/src/commons/sagas/WorkspaceSaga.ts
+++ b/src/commons/sagas/WorkspaceSaga.ts
@@ -48,7 +48,6 @@ import {
getRestoreExtraMethodsString,
getStoreExtraMethodsString,
highlightLine,
- inspectorUpdate,
makeElevatedContext,
visualizeEnv
} from '../utils/JsSlangHelper';
@@ -278,7 +277,6 @@ export default function* WorkspaceSaga(): SagaIterator {
const workspaceLocation = action.payload.workspaceLocation;
context = yield select((state: OverallState) => state.workspaces[workspaceLocation].context);
yield put(actions.clearReplOutput(workspaceLocation));
- inspectorUpdate(undefined);
highlightLine(undefined);
yield put(actions.clearReplOutput(workspaceLocation));
context.runtime.break = false;
@@ -564,7 +562,6 @@ function* updateInspector(workspaceLocation: WorkspaceLocation): SagaIterator {
const start = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1;
const end = lastDebuggerResult.context.runtime.nodes[0].loc.end.line - 1;
yield put(actions.highlightEditorLine([start, end], workspaceLocation));
- inspectorUpdate(lastDebuggerResult);
visualizeEnv(lastDebuggerResult);
} catch (e) {
yield put(actions.highlightEditorLine([], workspaceLocation));
@@ -613,10 +610,6 @@ export function* evalCode(
): SagaIterator {
context.runtime.debuggerOn =
(actionType === EVAL_EDITOR || actionType === DEBUG_RESUME) && context.chapter > 2;
- if (!context.runtime.debuggerOn && context.chapter > 2 && actionType !== EVAL_SILENT) {
- // Interface not guaranteed to exist, e.g. mission editor.
- inspectorUpdate(undefined); // effectively resets the interface
- }
// Logic for execution of substitution model visualizer
const correctWorkspace = workspaceLocation === 'playground' || workspaceLocation === 'sicp';
diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts
index 1760331b2b..878fffc0d2 100644
--- a/src/commons/sagas/__tests__/WorkspaceSaga.ts
+++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts
@@ -77,7 +77,6 @@ function generateDefaultState(
beforeEach(() => {
// Mock the inspector
(window as any).Inspector = jest.fn();
- (window as any).Inspector.updateContext = jest.fn();
(window as any).Inspector.highlightClean = jest.fn();
(window as any).Inspector.highlightLine = jest.fn();
});
diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx
index 54945c0eac..c289ae98e2 100644
--- a/src/commons/sideContent/SideContent.tsx
+++ b/src/commons/sideContent/SideContent.tsx
@@ -134,7 +134,7 @@ const SideContent = (props: SideContentProps) => {
/**
* Remove the 'side-content-tab-alert' class that causes tabs flash.
* To be run when tabs are changed.
- * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs.
+ * Currently this style is only used for the "Env Visualizer" tab.
*/
const resetAlert = (prevTabId: TabId) => {
const iconId = generateIconId(prevTabId);
diff --git a/src/commons/sideContent/SideContentInspector.tsx b/src/commons/sideContent/SideContentInspector.tsx
deleted file mode 100644
index 0099fbc583..0000000000
--- a/src/commons/sideContent/SideContentInspector.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Classes, NonIdealState, Spinner } from '@blueprintjs/core';
-import * as React from 'react';
-
-type State = {
- loading: boolean;
-};
-
-class SideContentInspector extends React.Component<{}, State> {
- private $parent: HTMLElement | null = null;
-
- constructor(props: any) {
- super(props);
- this.state = { loading: true };
- }
-
- public componentDidMount() {
- this.tryToLoad();
- }
-
- public render() {
- return (
- (this.$parent = r)} className="sa-inspector bp3-dark">
-
- The inspector generates a list of variable bindings based on breakpoints set in the
- editor.
-
-
- It is activated by clicking on the gutter of the editor (where all the line numbers are,
- on the left) to set a breakpoint, and then running the program. Only the first line of a
- statement can have a breakpoint. The program halts just before the statement is evaluated.
-
- {this.state.loading && (
-
} />
- )}
-
- );
- }
-
- private tryToLoad = () => {
- const element = (window as any).Inspector;
- if (this.$parent && element) {
- // Inspector has been loaded into the DOM
- element.init(this.$parent);
- this.setState((state, props) => {
- return { loading: false };
- });
- } else {
- // Try again in 1 second
- window.setTimeout(this.tryToLoad, 1000);
- }
- };
-}
-
-export default SideContentInspector;
diff --git a/src/commons/sideContent/SideContentSubstVisualizer.tsx b/src/commons/sideContent/SideContentSubstVisualizer.tsx
index 96db593227..acdfb2f487 100644
--- a/src/commons/sideContent/SideContentSubstVisualizer.tsx
+++ b/src/commons/sideContent/SideContentSubstVisualizer.tsx
@@ -1,6 +1,5 @@
/* eslint-disable simple-import-sort/imports */
import { Card, Classes, Divider, Pre, Slider, Button, ButtonGroup } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
import AceEditor from 'react-ace';
import { HotKeys } from 'react-hotkeys';
@@ -9,8 +8,6 @@ import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/
import 'js-slang/dist/editors/ace/theme/source';
import { IStepperPropContents } from 'js-slang/dist/stepper/stepper';
-import controlButton from '../ControlButton';
-
const SubstDefaultText = () => {
return (
@@ -32,24 +29,27 @@ const SubstDefaultText = () => {
Some useful keyboard shortcuts:
- {controlButton('(Comma)', IconNames.LESS_THAN)}: Move to the first step
+ a: Move to the first step
- {controlButton('(Period)', IconNames.GREATER_THAN)}: Move to the last step
+ e: Move to the last step
+ f: Move to the next step
- Note that the first and last step shortcuts are only active when the browser focus is on
- this panel (click on the slider or the text!).
+ b: Move to the previous step
- When the focus is on the slider, the arrow keys may also be used to move a single step.
+ Note that these shortcuts are only active when the browser focus is on this tab (click on or
+ above the explanation text).
);
};
const substKeyMap = {
- FIRST_STEP: ',',
- LAST_STEP: '.'
+ FIRST_STEP: 'a',
+ NEXT_STEP: 'f',
+ PREVIOUS_STEP: 'b',
+ LAST_STEP: 'e'
};
const SubstCodeDisplay = (props: { content: string }) => {
@@ -89,10 +89,14 @@ class SideContentSubstVisualizer extends React.Component {},
+ NEXT_STEP: () => {},
+ PREVIOUS_STEP: () => {},
LAST_STEP: () => {}
};
// console.log(this.props.content);
@@ -246,11 +250,16 @@ class SideContentSubstVisualizer extends React.Component {
- this.sliderShift(this.state.value - 1);
+ if (this.state.value !== 1) {
+ this.sliderShift(this.state.value - 1);
+ }
};
private stepNext = () => {
- this.sliderShift(this.state.value + 1);
+ const lastStepValue = this.props.content.length;
+ if (this.state.value !== lastStepValue) {
+ this.sliderShift(this.state.value + 1);
+ }
};
private stepPreviousFunctionCall = (value: number) => () => {
diff --git a/src/commons/sideContent/SideContentTypes.ts b/src/commons/sideContent/SideContentTypes.ts
index 71c6dac1b8..23a13fe54f 100644
--- a/src/commons/sideContent/SideContentTypes.ts
+++ b/src/commons/sideContent/SideContentTypes.ts
@@ -23,7 +23,6 @@ export enum SideContentType {
envVisualizer = 'env_visualizer',
grading = 'grading',
introduction = 'introduction',
- inspector = 'inspector',
module = 'module',
questionOverview = 'question_overview',
remoteExecution = 'remote_execution',
diff --git a/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap b/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap
index cad1678202..703e30075b 100644
--- a/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap
+++ b/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap
@@ -11,7 +11,7 @@ exports[`EnvVisualizer component renders correctly 1`] = `
The environment model diagram follows a notation introduced in
-
+
Structure and Interpretation of Computer Programs, JavaScript Adaptation, Chapter 3, Section 2
diff --git a/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx b/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx
index a04578f4a0..25aa14b7a7 100644
--- a/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx
+++ b/src/commons/sideContent/githubAssessments/SideContentMissionEditor.tsx
@@ -1,5 +1,4 @@
-import { InputGroup, Label } from '@blueprintjs/core';
-import { DatePicker } from '@blueprintjs/datetime';
+import { Label } from '@blueprintjs/core';
import { Variant } from 'js-slang/dist/types';
import React from 'react';
@@ -14,41 +13,13 @@ export type SideContentMissionEditorProps = {
};
const SideContentMissionEditor: React.FC = props => {
- const datePicker =
- props.missionMetadata.dueDate.getFullYear() > new Date().getFullYear() ? (
-
- ) : (
-
- );
-
return (
- Title
- Cover Image Link
- Summary
- Type
- ID
Source Version
- Reading
- Due Date
-
-
-
-
-
= props
disabled={false}
handleChapterSelect={handleChapterSelect}
/>
-
- {datePicker}
@@ -69,37 +38,9 @@ const SideContentMissionEditor: React.FC = props
props.setMissionMetadata(newMetadata);
}
- function handleChangeMissionTitle(event: any) {
- setMissionMetadataWrapper('title', event.target.value);
- }
-
- function handleChangeCoverImageLink(event: any) {
- setMissionMetadataWrapper('coverImage', event.target.value);
- }
-
- function handleChangeMissionSummary(event: any) {
- setMissionMetadataWrapper('webSummary', event.target.value);
- }
-
function handleChapterSelect(i: SourceLanguage, e?: React.SyntheticEvent) {
setMissionMetadataWrapper('sourceVersion', i.chapter);
}
-
- function handleChangeMissionId(event: any) {
- setMissionMetadataWrapper('id', event.target.value);
- }
-
- function handleChangeReading(event: any) {
- setMissionMetadataWrapper('reading', event.target.value);
- }
-
- function handleChangeType(event: any) {
- setMissionMetadataWrapper('type', event.target.value);
- }
-
- function handleDateChange(date: Date) {
- setMissionMetadataWrapper('dueDate', date);
- }
};
export default SideContentMissionEditor;
diff --git a/src/commons/sideContent/githubAssessments/__tests__/SideContentMissionEditor.tsx b/src/commons/sideContent/githubAssessments/__tests__/SideContentMissionEditor.tsx
deleted file mode 100644
index 8ebb0d9cd6..0000000000
--- a/src/commons/sideContent/githubAssessments/__tests__/SideContentMissionEditor.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { act, render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import { MissionMetadata } from '../../../githubAssessments/GitHubMissionTypes';
-import SideContentMissionEditor from '../SideContentMissionEditor';
-
-test('typing into SideContentMissionEditor text boxes triggers setter function', () => {
- const missionMetadata = {
- coverImage: 'dummyCoverImage',
- type: 'dummyType',
- id: 'dummyNumber',
- title: 'dummyTitle',
- sourceVersion: 1,
- dueDate: new Date(),
-
- reading: 'dummyReading',
- webSummary: 'dummySummary'
- } as MissionMetadata;
-
- let outsideValue = Object.assign({}, missionMetadata);
-
- const setMissionMetadata = jest.fn((insideValue: MissionMetadata) => {
- outsideValue = insideValue;
- });
-
- act(() => {
- render(
-
- );
- });
-
- const coverImageText = screen.getByDisplayValue('dummyCoverImage');
- userEvent.clear(coverImageText);
- userEvent.type(coverImageText, 'realCoverImage');
- expect(outsideValue.coverImage).toBe('realCoverImage');
-
- const typeText = screen.getByDisplayValue('dummyType');
- userEvent.clear(typeText);
- userEvent.type(typeText, 'realType');
- expect(outsideValue.type).toBe('realType');
-
- const numberText = screen.getByDisplayValue('dummyNumber');
- userEvent.clear(numberText);
- userEvent.type(numberText, 'realNumber');
- expect(outsideValue.id).toBe('realNumber');
-
- const titleText = screen.getByDisplayValue('dummyTitle');
- userEvent.clear(titleText);
- userEvent.type(titleText, 'realTitle');
- expect(outsideValue.title).toBe('realTitle');
-
- const readingText = screen.getByDisplayValue('dummyReading');
- userEvent.clear(readingText);
- userEvent.type(readingText, 'realReading');
- expect(outsideValue.reading).toBe('realReading');
-
- const summaryText = screen.getByDisplayValue('dummySummary');
- userEvent.clear(summaryText);
- userEvent.type(summaryText, 'realSummary');
- expect(outsideValue.webSummary).toBe('realSummary');
-});
diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts
index 4af409aa82..2d83f2011a 100644
--- a/src/commons/utils/Constants.ts
+++ b/src/commons/utils/Constants.ts
@@ -32,7 +32,7 @@ const googleAppId = process.env.REACT_APP_GOOGLE_APP_ID;
const githubClientId = process.env.REACT_APP_GITHUB_CLIENT_ID || '';
const githubOAuthProxyUrl = process.env.REACT_APP_GITHUB_OAUTH_PROXY_URL || '';
const interactiveSicpDataUrl =
- process.env.REACT_APP_INTERACTIVE_SICP_DATA_URL || 'https://source-academy.github.io/sicp/'; // data for interactive-sicp (images and json files)
+ process.env.REACT_APP_INTERACTIVE_SICP_DATA_URL || 'https://sicp.sourceacademy.org/'; // data for sicpjs (images and json files)
const authProviders: Map =
new Map();
@@ -77,26 +77,26 @@ export enum Links {
piazza = 'https://piazza.com/class/kas136yscf8605',
sourceAcademyAssets = 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com',
- sourceDocs = 'https://source-academy.github.io/source/',
+ sourceDocs = 'https://docs.sourceacademy.org/',
techSVC = 'mailto:techsvc@comp.nus.edu.sg',
techSVCNumber = '6516 2736',
- textbook = 'https://source-academy.github.io/interactive-sicp/',
- textbookChapter2_2 = 'https://source-academy.github.io/interactive-sicp/2.2',
- textbookChapter3_2 = 'https://source-academy.github.io/interactive-sicp/3.2',
-
+ textbook = 'https://sourceacademy.org/sicpjs/',
+ playground = 'https://sourceacademy.org/playground',
+ textbookChapter2_2 = 'https://sourceacademy.org/sicpjs/2.2',
+ textbookChapter3_2 = 'https://sourceacademy.org/sicpjs/3.2',
aceHotkeys = 'https://github.com/ajaxorg/ace/wiki/Default-Keyboard-Shortcuts',
sourceHotkeys = 'https://github.com/source-academy/cadet-frontend/wiki/Source-Academy-Keyboard-Shortcuts',
- source_1 = 'https://source-academy.github.io/source/source_1/',
- source_1_Lazy = 'https://source-academy.github.io/source/source_1_lazy/',
- source_1_Wasm = 'https://source-academy.github.io/source/source_1_wasm/',
- source_2 = 'https://source-academy.github.io/source/source_2/',
- source_2_Lazy = 'https://source-academy.github.io/source/source_2_lazy/',
- source_3 = 'https://source-academy.github.io/source/source_3/',
- source_3_Concurrent = 'https://source-academy.github.io/source/source_3_concurrent/',
- source_3_Nondet = 'https://source-academy.github.io/source/source_3_non-det/',
- source_4 = 'https://source-academy.github.io/source/source_4/',
- source_4_Gpu = 'https://source-academy.github.io/source/source_4_gpu/'
+ source_1 = 'https://docs.sourceacademy.org/source_1/',
+ source_1_Lazy = 'https://docs.sourceacademy.org/source_1_lazy/',
+ source_1_Wasm = 'https://docs.sourceacademy.org/source_1_wasm/',
+ source_2 = 'https://docs.sourceacademy.org/source_2/',
+ source_2_Lazy = 'https://docs.sourceacademy.org/source_2_lazy/',
+ source_3 = 'https://docs.sourceacademy.org/source_3/',
+ source_3_Concurrent = 'https://docs.sourceacademy.org/source_3_concurrent/',
+ source_3_Nondet = 'https://docs.sourceacademy.org/source_3_non-det/',
+ source_4 = 'https://docs.sourceacademy.org/source_4/',
+ source_4_Gpu = 'https://docs.sourceacademy.org/source_4_gpu/'
}
const Constants = {
diff --git a/src/commons/utils/JsSlangHelper.ts b/src/commons/utils/JsSlangHelper.ts
index c9ad338dda..33ecb46f90 100644
--- a/src/commons/utils/JsSlangHelper.ts
+++ b/src/commons/utils/JsSlangHelper.ts
@@ -130,14 +130,6 @@ export function highlightLine(line: number | undefined) {
}
}
-export function inspectorUpdate(context: Context | undefined) {
- if ((window as any).Inspector) {
- (window as any).Inspector.updateContext(context, stringify);
- } else {
- throw new Error('Inspector not loaded');
- }
-}
-
export const externalBuiltIns = {
display,
rawDisplay,
diff --git a/src/commons/utils/ParamParseHelper.ts b/src/commons/utils/ParamParseHelper.ts
index 55ddb79070..5a6db7a457 100644
--- a/src/commons/utils/ParamParseHelper.ts
+++ b/src/commons/utils/ParamParseHelper.ts
@@ -13,6 +13,8 @@ import { AssessmentCategories, AssessmentCategory } from '../assessment/Assessme
export const assessmentCategoryLink = (cat: AssessmentCategory): string =>
cat === AssessmentCategories.Sidequest ? 'quests' : cat.toLowerCase().concat('s');
+export const assessmentTypeLink = (type: string): string => type.toLowerCase().replace(/\W+/g, '_');
+
/** Converts an optinal string
* parameter into an integer or null value.
*
diff --git a/src/features/sicp/TableOfContentsHelper.ts b/src/features/sicp/TableOfContentsHelper.ts
new file mode 100644
index 0000000000..1616d6947c
--- /dev/null
+++ b/src/features/sicp/TableOfContentsHelper.ts
@@ -0,0 +1,13 @@
+import tocNavigation from './data/toc-navigation.json';
+
+export const getNext = (section: string): string | undefined => {
+ const node = tocNavigation[section];
+
+ return node && node['next'];
+};
+
+export const getPrev = (section: string): string | undefined => {
+ const node = tocNavigation[section];
+
+ return node && node['prev'];
+};
diff --git a/src/features/sicp/__tests__/TableOfContentsHelper.ts b/src/features/sicp/__tests__/TableOfContentsHelper.ts
new file mode 100644
index 0000000000..486875b24a
--- /dev/null
+++ b/src/features/sicp/__tests__/TableOfContentsHelper.ts
@@ -0,0 +1,31 @@
+import { getNext, getPrev } from '../TableOfContentsHelper';
+
+const data = {
+ '1': { next: '2' },
+ '2': { next: '3', prev: '1' },
+ '3': { prev: '2' }
+};
+
+jest.mock('../data/toc-navigation.json', () => data);
+
+describe('Table of contents helper', () => {
+ test('generate next correctly', () => {
+ expect(getNext('1')).toBe('2');
+ expect(getNext('2')).toBe('3');
+ expect(getNext('3')).toBeUndefined();
+ });
+
+ test('generate prev correctly', () => {
+ expect(getPrev('1')).toBeUndefined();
+ expect(getPrev('2')).toBe('1');
+ expect(getPrev('3')).toBe('2');
+ });
+
+ test('handle invalid values correctly', () => {
+ expect(getNext('invalid')).toBeUndefined();
+ expect(getNext('')).toBeUndefined();
+
+ expect(getPrev('invalid')).toBeUndefined();
+ expect(getPrev('')).toBeUndefined();
+ });
+});
diff --git a/src/features/sicp/errors/SicpErrorBoundary.tsx b/src/features/sicp/errors/SicpErrorBoundary.tsx
new file mode 100644
index 0000000000..771d2685d3
--- /dev/null
+++ b/src/features/sicp/errors/SicpErrorBoundary.tsx
@@ -0,0 +1,36 @@
+import { Component, ErrorInfo, ReactNode } from 'react';
+
+import getSicpError, { SicpErrorType } from './SicpErrors';
+
+type Props = {
+ children: ReactNode;
+};
+
+type State = {
+ hasError: boolean;
+};
+
+class SicpErrorBoundary extends Component {
+ public state: State = {
+ hasError: false
+ };
+
+ public static getDerivedStateFromError(_: Error): State {
+ // Update state so the next render will show the fallback UI.
+ return { hasError: true };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('Uncaught error:', error, errorInfo);
+ }
+
+ public render() {
+ if (this.state.hasError) {
+ return getSicpError(SicpErrorType.UNEXPECTED_ERROR);
+ }
+
+ return this.props.children;
+ }
+}
+
+export default SicpErrorBoundary;
diff --git a/src/features/sicp/errors/SicpErrors.tsx b/src/features/sicp/errors/SicpErrors.tsx
new file mode 100644
index 0000000000..a4f67b533d
--- /dev/null
+++ b/src/features/sicp/errors/SicpErrors.tsx
@@ -0,0 +1,58 @@
+import { NonIdealState } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+
+export enum SicpErrorType {
+ UNEXPECTED_ERROR,
+ PAGE_NOT_FOUND_ERROR,
+ PARSING_ERROR
+}
+
+const unexpectedError = (
+
+);
+
+const pageNotFoundError = (
+
+);
+
+const parsingError = (
+
+);
+
+const errorComponent = (description: JSX.Element) => (
+
+);
+
+const getSicpError = (type: SicpErrorType) => {
+ switch (type) {
+ case SicpErrorType.PAGE_NOT_FOUND_ERROR:
+ return errorComponent(pageNotFoundError);
+ case SicpErrorType.PARSING_ERROR:
+ return errorComponent(parsingError);
+ default:
+ // handle unexpected error case
+ return errorComponent(unexpectedError);
+ }
+};
+
+export default getSicpError;
diff --git a/src/features/sicp/errors/__tests__/SicpErrors.tsx b/src/features/sicp/errors/__tests__/SicpErrors.tsx
new file mode 100644
index 0000000000..690623d94f
--- /dev/null
+++ b/src/features/sicp/errors/__tests__/SicpErrors.tsx
@@ -0,0 +1,20 @@
+import { mount } from 'enzyme';
+
+import getSicpError, { SicpErrorType } from '../SicpErrors';
+
+describe('Sicp errors:', () => {
+ test('unexpected error renders correctly', () => {
+ const tree = mount(getSicpError(SicpErrorType.UNEXPECTED_ERROR));
+ expect(tree.debug()).toMatchSnapshot();
+ });
+
+ test('page not found error renders correctly', () => {
+ const tree = mount(getSicpError(SicpErrorType.PAGE_NOT_FOUND_ERROR));
+ expect(tree.debug()).toMatchSnapshot();
+ });
+
+ test('unexpected error renders correctly', () => {
+ const tree = mount(getSicpError(SicpErrorType.PARSING_ERROR));
+ expect(tree.debug()).toMatchSnapshot();
+ });
+});
diff --git a/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap b/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap
new file mode 100644
index 0000000000..832e83e25a
--- /dev/null
+++ b/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Sicp errors: page not found error renders correctly 1`] = `
+"
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+ Something went wrong :(
+
+
+
+
+ "
+`;
+
+exports[`Sicp errors: unexpected error renders correctly 1`] = `
+"
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+ Something went wrong :(
+
+
+
+
+ "
+`;
+
+exports[`Sicp errors: unexpected error renders correctly 2`] = `
+"
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+ Something went wrong :(
+
+
+
+
+ "
+`;
diff --git a/src/features/sicp/parser/ParseJson.tsx b/src/features/sicp/parser/ParseJson.tsx
index 336f5521ca..b3e7c5cd5a 100644
--- a/src/features/sicp/parser/ParseJson.tsx
+++ b/src/features/sicp/parser/ParseJson.tsx
@@ -42,7 +42,8 @@ const handleFootnote = (obj: JsonType, refs: React.MutableRefObject<{}>) => {
return (
{obj['count'] === 1 &&
}
-
(refs.current[obj['id']!] = ref)} className="sicp-footnote">
+
+
@@ -104,7 +105,7 @@ const handleSnippet = (obj: JsonType) => {
body: obj['body']!,
id: obj['id']!,
initialEditorValueHash: obj['withoutPrepend']!,
- initialFullProgramHash: obj['program']!,
+ initialFullProgramHash: obj['program']! || obj['withoutPrepend']!,
initialPrependHash: obj['prepend']!,
output: obj['output']!
};
@@ -113,7 +114,8 @@ const handleSnippet = (obj: JsonType) => {
};
const handleFigure = (obj: JsonType, refs: React.MutableRefObject<{}>) => (
-
(refs.current[obj['id']!] = ref)} className="sicp-figure">
+
+
(refs.current[obj['id']!] = ref)} />
{handleImage(obj, refs)}
{obj['captionName'] && (
@@ -130,7 +132,7 @@ const handleImage = (obj: JsonType, refs: React.MutableRefObject<{}>) => {
);
} else if (obj['snippet']) {
@@ -152,7 +154,8 @@ const handleTD = (obj: JsonType, refs: React.MutableRefObject<{}>, index: intege
const handleExercise = (obj: JsonType, refs: React.MutableRefObject<{}>) => {
return (
- (refs.current[obj['id']!] = ref)}>
+
+
(refs.current[obj['id']!] = ref)} />
) => (
<>
- (refs.current[obj['id']!] = ref)} className="sicp-text">
+
+
(refs.current[obj['id']!] = ref)} />
{parseArr(obj['child']!, refs)}
diff --git a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap
index d4e6b50d93..ad006b0bca 100644
--- a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap
+++ b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap
@@ -115,18 +115,21 @@ exports[`Parse epigraph EPIGRAPH with title successful 1`] = `
exports[`Parse exercise EXERCISE with solution successful 1`] = `
"
"
`;
exports[`Parse exercise EXERCISE without solution successful 1`] = `
"
"
`;
exports[`Parse figures FIGURE with image and scale successful 1`] = `
"
+
name
@@ -141,7 +144,8 @@ exports[`Parse figures FIGURE with image and scale successful 1`] = `
exports[`Parse figures FIGURE with image successful 1`] = `
"
-
+
+
name
@@ -155,7 +159,8 @@ exports[`Parse figures FIGURE with image successful 1`] = `
exports[`Parse figures FIGURE with snippet successful 1`] = `
"
-
+
+
name
@@ -169,6 +174,7 @@ exports[`Parse figures FIGURE with snippet successful 1`] = `
exports[`Parse figures FIGURE with table successful 1`] = `
"
+
@@ -220,6 +226,7 @@ exports[`Parse footnote DISPLAYFOOTNOTE count is 1 successful 1`] = `
"
+
[1]
@@ -235,6 +242,7 @@ exports[`Parse footnote DISPLAYFOOTNOTE count is 1 successful 1`] = `
exports[`Parse footnote DISPLAYFOOTNOTE count is 2 successful 1`] = `
"
+
[2]
@@ -364,6 +372,7 @@ exports[`Parse section SECTION successful 1`] = `
+
Mock Text
@@ -379,6 +388,7 @@ exports[`Parse section SECTION successful 1`] = `
+
Mock Text
@@ -415,13 +425,13 @@ exports[`Parse snippet SNIPPET with prepend successful 1`] = `
`;
exports[`Parse snippet SNIPPET without prepend successful 1`] = `
-"
+"
Code Snippet
"
`;
exports[`Parse snippet SNIPPET without prepend with output successful 1`] = `
-"
+"
Code Snippet
"
`;
diff --git a/src/pages/academy/sourcereel/Sourcereel.tsx b/src/pages/academy/sourcereel/Sourcereel.tsx
index eb766766dd..b3f3c7eabe 100644
--- a/src/pages/academy/sourcereel/Sourcereel.tsx
+++ b/src/pages/academy/sourcereel/Sourcereel.tsx
@@ -14,7 +14,6 @@ import { ControlBarExternalLibrarySelect } from '../../../commons/controlBar/Con
import { HighlightedLines, Position } from '../../../commons/editor/EditorTypes';
import SideContentDataVisualizer from '../../../commons/sideContent/SideContentDataVisualizer';
import SideContentEnvVisualizer from '../../../commons/sideContent/SideContentEnvVisualizer';
-import SideContentInspector from '../../../commons/sideContent/SideContentInspector';
import { SideContentTab, SideContentType } from '../../../commons/sideContent/SideContentTypes';
import SourceRecorderControlBar, {
SourceRecorderControlBarProps
@@ -345,7 +344,6 @@ class Sourcereel extends React.Component
{
toSpawn: () => true
},
dataVisualizerTab,
- inspectorTab,
envVisualizerTab
],
workspaceLocation: 'sourcereel'
@@ -406,14 +404,6 @@ const dataVisualizerTab: SideContentTab = {
toSpawn: () => true
};
-const inspectorTab: SideContentTab = {
- label: 'Inspector',
- iconName: IconNames.SEARCH,
- body: ,
- id: SideContentType.inspector,
- toSpawn: () => true
-};
-
const envVisualizerTab: SideContentTab = {
label: 'Env Visualizer',
iconName: IconNames.GLOBE,
diff --git a/src/pages/contributors/subcomponents/ContributorsDetails.tsx b/src/pages/contributors/subcomponents/ContributorsDetails.tsx
index b572555f0d..22b935463c 100644
--- a/src/pages/contributors/subcomponents/ContributorsDetails.tsx
+++ b/src/pages/contributors/subcomponents/ContributorsDetails.tsx
@@ -11,45 +11,48 @@ class ContributorsDetails extends React.Component {
return (
- The People behind Source Academy
+ The Team behind the Source Academy
- The Source Academy is designed by and for students of the National University of
- Singapore. Students who completed the CS1101S module come back to coach their juniors as
- "Avengers" or to further develop and improve the Academy. This page includes all
- developers who contributed to the Source Academy Knight (2020) and its its
- precursor, Cadet (2018). Both of these succeeded Source Academy 2 (2017) and
- ultimately the original Source Academy (2016).
+ The Source Academy is designed and developed by a team of students, most of who
+ have used the system to learn the fundamentals of computing and enjoyed it. This page
+ includes all developers who contributed to the Source Academy Rook (2022) and its
+ precursors Knight (2020) and Cadet (2018). These versions succeeded Source
+ Academy 2 (2017) and ultimately the original Source Academy (2016).
- 2021 Leadership
+ 2022 Leadership
- Tiffany Chong
+ Tee Hao Wei
+
+ (CTO)
+
+ {dot}
+
+ Gokul Rajiv
(Game)
{dot}
- Anthony Halim
+ Chow En Rong
(Frontend)
{dot}
-
- Daryl Tan,
-
+
Thomas Tan
(Source)
{dot}
-
- Tee Hao Wei
+
+ Chen Yanyu
- (Backend & DevOps)
+ (Backend)
{dot}
@@ -124,8 +127,8 @@ class ContributorsDetails extends React.Component {
-
- 2020 Leadership
+
+ 2020 Leadership (Knight)
@@ -164,6 +167,35 @@ class ContributorsDetails extends React.Component {
(Backend & DevOps)
+
+
+ 2021 Leadership
+
+
+
+ Tiffany Chong
+
+ (Game)
+
+ {dot}
+
+ Anthony Halim
+
+ (Frontend)
+
+ {dot}
+
+ Daryl Tan, Thomas Tan
+
+ (Source)
+
+ {dot}
+
+ Tee Hao Wei
+
+ (Backend & DevOps)
+
+
diff --git a/src/pages/githubAssessments/GitHubAssessmentDefaultValues.ts b/src/pages/githubAssessments/GitHubAssessmentDefaultValues.ts
index ec1468b695..b7e4cd2b72 100644
--- a/src/pages/githubAssessments/GitHubAssessmentDefaultValues.ts
+++ b/src/pages/githubAssessments/GitHubAssessmentDefaultValues.ts
@@ -70,14 +70,7 @@ If you need a more detailed cheatsheet, please click [here](https://www.markdown
export const defaultStarterCode = '// Your program here!\n';
export const defaultMissionMetadata = {
- coverImage: '',
- type: '',
- id: '',
- title: '',
- sourceVersion: 1,
- dueDate: new Date(8640000000000000),
- reading: '',
- webSummary: ''
+ sourceVersion: 1
} as MissionMetadata;
export const defaultTask = {
diff --git a/src/pages/githubAssessments/GitHubAssessmentListing.tsx b/src/pages/githubAssessments/GitHubAssessmentListing.tsx
new file mode 100644
index 0000000000..8c65a0737b
--- /dev/null
+++ b/src/pages/githubAssessments/GitHubAssessmentListing.tsx
@@ -0,0 +1,162 @@
+import {
+ Button,
+ Card,
+ Elevation,
+ H4,
+ H6,
+ Icon,
+ NonIdealState,
+ Spinner,
+ Text
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { useMemo } from 'react';
+import { useMediaQuery } from 'react-responsive';
+
+import defaultCoverImage from '../../assets/default_cover_image.jpg';
+import ContentDisplay from '../../commons/ContentDisplay';
+import Markdown from '../../commons/Markdown';
+import Constants from '../../commons/utils/Constants';
+import { history } from '../../commons/utils/HistoryHelper';
+import { GHAssessmentOverview } from './GitHubClassroom';
+
+type GitHubAssessmentListingProps = {
+ assessmentOverviews?: GHAssessmentOverview[];
+ refreshAssessmentOverviews: () => void;
+};
+
+/**
+ * A page that lists the missions available to the authenticated user.
+ * This page should only be reachable if using a GitHub-hosted deployment.
+ */
+const GitHubAssessmentListing: React.FC = props => {
+ const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
+
+ let display: JSX.Element;
+
+ const createAssessmentButton = useMemo(
+ () => (
+ history.push(`/githubassessments/editor`)}>
+ Create a New Assessment!
+
+ ),
+ []
+ );
+
+ const refreshButton = useMemo(
+ () => (
+
+ Refresh Assessments
+
+ ),
+ [props.refreshAssessmentOverviews]
+ );
+
+ if (!props.assessmentOverviews) {
+ display = (
+ <>
+ {createAssessmentButton}
+ } />
+ >
+ );
+ } else if (props.assessmentOverviews.length === 0) {
+ display = (
+ <>
+ {createAssessmentButton}
+ {refreshButton}
+
+ >
+ );
+ } else {
+ // Create cards
+ const cards = props.assessmentOverviews.map(element =>
+ convertAssessmentOverviewToCard(element, isMobileBreakpoint)
+ );
+ display = (
+ <>
+ {createAssessmentButton}
+ {refreshButton}
+ {cards}
+ >
+ );
+ }
+
+ return (
+
+ {}} />
+
+ );
+};
+
+/**
+ * Maps from a BrowsableMission object to a JSX card that can be displayed on the Mission Listing.
+ *
+ * @param missionRepo The BrowsableMission representation of a single mission repository
+ * @param isMobileBreakpoint Whether we are using mobile breakpoint
+ */
+function convertAssessmentOverviewToCard(
+ assessmentOverview: GHAssessmentOverview,
+ isMobileBreakpoint: boolean
+) {
+ const ratio = isMobileBreakpoint ? 5 : 3;
+ const ownerSlashName =
+ assessmentOverview.missionRepoData.repoOwner +
+ '/' +
+ assessmentOverview.missionRepoData.repoName;
+ const dueDate = assessmentOverview.dueDate.toDateString();
+
+ const hasDueDate = new Date(8640000000000000) > assessmentOverview.dueDate;
+ const isOverdue = new Date() > assessmentOverview.dueDate;
+
+ const assessmentNotAccepted = assessmentOverview.link !== undefined;
+ let buttonText = 'Open';
+ let handleClick = () => history.push(`/githubassessments/editor`, assessmentOverview);
+
+ if (assessmentNotAccepted) {
+ buttonText = 'Accept';
+ handleClick = () => window.open(assessmentOverview.link);
+ } else if (isOverdue) {
+ buttonText = 'Review Answers';
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {assessmentOverview.title}
+ {ownerSlashName}
+
+
+
+
+
+
+
+
+
+
+ {hasDueDate ? 'Due: ' + dueDate : 'No due date'}
+
+
+
+ {buttonText}
+
+
+
+
+
+
+ );
+}
+
+export default GitHubAssessmentListing;
diff --git a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx
index f7b3baa7e5..8398810262 100644
--- a/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx
+++ b/src/pages/githubAssessments/GitHubAssessmentWorkspace.tsx
@@ -45,11 +45,6 @@ import {
discoverFilesToBeCreatedWithoutMissionRepoData,
getMissionData
} from '../../commons/githubAssessments/GitHubMissionDataUtils';
-import {
- GitHubMissionSaveDialog,
- GitHubMissionSaveDialogProps,
- GitHubMissionSaveDialogResolution
-} from '../../commons/githubAssessments/GitHubMissionSaveDialog';
import {
MissionData,
MissionMetadata,
@@ -86,6 +81,7 @@ import {
defaultMissionMetadata,
defaultTask
} from './GitHubAssessmentDefaultValues';
+import { GHAssessmentOverview } from './GitHubClassroom';
export type GitHubAssessmentWorkspaceProps = DispatchProps & StateProps & RouteComponentProps;
@@ -136,7 +132,7 @@ const GitHubAssessmentWorkspace: React.FC = prop
const octokit = getGitHubOctokitInstance();
if (octokit === undefined) {
- history.push('/githubassessments/missions');
+ history.push('/githubassessments/login');
}
/**
@@ -164,9 +160,10 @@ const GitHubAssessmentWorkspace: React.FC = prop
const [currentTaskIsMCQ, setCurrentTaskIsMCQ] = React.useState(false);
const [displayMCQInEditor, setDisplayMCQInEditor] = React.useState(true);
const [mcqQuestion, setMCQQuestion] = React.useState(defaultMCQQuestion);
- const [missionRepoData, setMissionRepoData] = React.useState(
- props.location.state as MissionRepoData
+ const [missionRepoData, setMissionRepoData] = React.useState(
+ undefined
);
+ const assessmentOverview = props.location.state as GHAssessmentOverview;
const [showBriefingOverlay, setShowBriefingOverlay] = React.useState(false);
const [selectedTab, setSelectedTab] = React.useState(SideContentType.questionOverview);
@@ -261,35 +258,22 @@ const GitHubAssessmentWorkspace: React.FC = prop
/**
* Sets up the workspace for when the user is retrieving Mission information from a GitHub repository
*/
- const setUpWithMissionRepoData = useCallback(async () => {
+ const setUpWithAssessmentOverview = useCallback(async () => {
if (octokit === undefined) return;
+ const missionRepoData = assessmentOverview.missionRepoData;
+
const missionDataPromise = getMissionData(missionRepoData, octokit);
- const isTeacherModePromise = octokit.users
- .getAuthenticated()
- .then((authenticatedUser: any) => {
- const userLogin = authenticatedUser.data.login;
- return userLogin === missionRepoData.repoOwner;
- })
- .then(async (userOwnsRepo: boolean) => {
- if (userOwnsRepo) return true;
-
- const userOrganisations = (await octokit.orgs.listForAuthenticatedUser()).data;
- let userOrganisationOwnsRepo = false;
- for (let i = 0; i < userOrganisations.length; i++) {
- const org = userOrganisations[i];
- // User has admin access to an organization owning the repo
- userOrganisationOwnsRepo = org.login === missionRepoData.repoOwner;
- if (userOrganisationOwnsRepo) {
- break;
- }
- }
- return userOrganisationOwnsRepo;
- });
+ const isTeacherModePromise = octokit.users.getAuthenticated().then((authenticatedUser: any) => {
+ const userLogin = authenticatedUser.data.login;
+ return userLogin === missionRepoData.repoOwner;
+ });
const promises = [missionDataPromise, isTeacherModePromise];
Promise.all(promises).then((promises: any[]) => {
+ setMissionRepoData(missionRepoData);
+
setHasUnsavedChangesToTasks(false);
setHasUnsavedChangesToBriefing(false);
setHasUnsavedChangesToMetadata(false);
@@ -316,12 +300,17 @@ const GitHubAssessmentWorkspace: React.FC = prop
setIsLoading(false);
});
- }, [changeStateDueToChangedTaskNumber, missionRepoData, octokit, handleUpdateHasUnsavedChanges]);
+ }, [
+ assessmentOverview,
+ octokit,
+ changeStateDueToChangedTaskNumber,
+ handleUpdateHasUnsavedChanges
+ ]);
/**
* Sets up the workspace for when the user is creating a new Mission
*/
- const setUpWithoutMissionRepoData = useCallback(() => {
+ const setUpWithoutAssessmentOverview = useCallback(() => {
setSummary(defaultMissionBriefing);
setMissionMetadata(defaultMissionMetadata);
@@ -346,12 +335,12 @@ const GitHubAssessmentWorkspace: React.FC = prop
}, [changeStateDueToChangedTaskNumber, handleUpdateHasUnsavedChanges]);
useEffect(() => {
- if (missionRepoData === undefined) {
- setUpWithoutMissionRepoData();
+ if (assessmentOverview === undefined) {
+ setUpWithoutAssessmentOverview();
} else {
- setUpWithMissionRepoData();
+ setUpWithAssessmentOverview();
}
- }, [missionRepoData, setUpWithMissionRepoData, setUpWithoutMissionRepoData]);
+ }, [assessmentOverview, setUpWithAssessmentOverview, setUpWithoutAssessmentOverview]);
const briefingOverlay = (
@@ -399,18 +388,20 @@ const GitHubAssessmentWorkspace: React.FC = prop
githubEmail: string | null,
commitMessage: string
) => {
+ const typedMissionRepoData = missionRepoData as MissionRepoData;
+
const { saveType } = await checkIfFileCanBeSavedAndGetSaveType(
octokit,
- missionRepoData.repoOwner,
- missionRepoData.repoName,
+ typedMissionRepoData.repoOwner,
+ typedMissionRepoData.repoName,
changedFile
);
if (saveType === 'Overwrite') {
await performOverwritingSave(
octokit,
- missionRepoData.repoOwner,
- missionRepoData.repoName,
+ typedMissionRepoData.repoOwner,
+ typedMissionRepoData.repoName,
changedFile,
githubName,
githubEmail,
@@ -422,8 +413,8 @@ const GitHubAssessmentWorkspace: React.FC = prop
if (saveType === 'Create') {
await performCreatingSave(
octokit,
- missionRepoData.repoOwner,
- missionRepoData.repoName,
+ typedMissionRepoData.repoOwner,
+ typedMissionRepoData.repoName,
changedFile,
githubName,
githubEmail,
@@ -442,10 +433,12 @@ const GitHubAssessmentWorkspace: React.FC = prop
githubEmail: string | null,
commitMessage: string
) => {
+ const typedMissionRepoData = missionRepoData as MissionRepoData;
+
await performFolderDeletion(
octokit,
- missionRepoData.repoOwner,
- missionRepoData.repoName,
+ typedMissionRepoData.repoOwner,
+ typedMissionRepoData.repoName,
fileName,
githubName,
githubEmail,
@@ -473,21 +466,7 @@ const GitHubAssessmentWorkspace: React.FC = prop
cachedTaskList,
isTeacherMode
);
- const changedFiles = Object.keys(filenameToContentMap).sort();
-
- const dialogResults = await promisifyDialog<
- GitHubMissionSaveDialogProps,
- GitHubMissionSaveDialogResolution
- >(GitHubMissionSaveDialog, resolve => ({
- repoName: missionRepoData.repoName,
- filesToChangeOrCreate: changedFiles,
- filesToDelete: foldersToDelete,
- resolveDialog: dialogResults => resolve(dialogResults)
- }));
-
- if (!dialogResults.confirmSave) {
- return;
- }
+ const changedFiles = Object.keys(filenameToContentMap);
type GetAuthenticatedResponse = GetResponseTypeFromEndpointMethod<
typeof octokit.users.getAuthenticated
@@ -495,7 +474,7 @@ const GitHubAssessmentWorkspace: React.FC = prop
const authUser: GetAuthenticatedResponse = await octokit.users.getAuthenticated();
const githubName = authUser.data.name;
const githubEmail = authUser.data.email;
- const commitMessage = dialogResults.commitMessage;
+ const commitMessage = '';
for (let i = 0; i < foldersToDelete.length; i++) {
await conductDelete(foldersToDelete[i], githubName, githubEmail, commitMessage);
@@ -515,17 +494,16 @@ const GitHubAssessmentWorkspace: React.FC = prop
setHasUnsavedChangesToBriefing(false);
setHasUnsavedChangesToMetadata(false);
}, [
+ octokit,
+ isTeacherMode,
briefingContent,
- cachedBriefingContent,
+ missionMetadata,
taskList,
cachedTaskList,
- missionMetadata,
+ cachedBriefingContent,
cachedMissionMetadata,
conductSave,
- conductDelete,
- octokit,
- missionRepoData,
- isTeacherMode
+ conductDelete
]);
/**
@@ -605,12 +583,17 @@ const GitHubAssessmentWorkspace: React.FC = prop
}, [briefingContent, missionMetadata, octokit, taskList]);
const onClickSave = useCallback(() => {
+ if (assessmentOverview !== undefined && new Date() > assessmentOverview.dueDate) {
+ showWarningMessage('It is past the due date for this assessment!');
+ return;
+ }
+
if (missionRepoData !== undefined) {
saveWithMissionRepoData();
} else {
saveWithoutMissionRepoData();
}
- }, [missionRepoData, saveWithMissionRepoData, saveWithoutMissionRepoData]);
+ }, [assessmentOverview, missionRepoData, saveWithMissionRepoData, saveWithoutMissionRepoData]);
const onClickReset = useCallback(async () => {
const confirmReset = await showSimpleConfirmDialog({
@@ -657,7 +640,14 @@ const GitHubAssessmentWorkspace: React.FC = prop
);
const onClickPrevious = useCallback(() => {
- if (shouldProceedToChangeTask(currentTaskNumber, taskList, cachedTaskList, missionRepoData)) {
+ if (
+ shouldProceedToChangeTask(
+ currentTaskNumber,
+ taskList,
+ cachedTaskList,
+ missionRepoData as MissionRepoData
+ )
+ ) {
let activeTaskList = taskList;
if (missionRepoData !== undefined) {
activeTaskList = cachedTaskList.map((taskData: TaskData) => Object.assign({}, taskData));
@@ -677,7 +667,14 @@ const GitHubAssessmentWorkspace: React.FC = prop
]);
const onClickNext = useCallback(() => {
- if (shouldProceedToChangeTask(currentTaskNumber, taskList, cachedTaskList, missionRepoData)) {
+ if (
+ shouldProceedToChangeTask(
+ currentTaskNumber,
+ taskList,
+ cachedTaskList,
+ missionRepoData as MissionRepoData
+ )
+ ) {
let activeTaskList = taskList;
if (missionRepoData !== undefined) {
activeTaskList = cachedTaskList.map((taskData: TaskData) => Object.assign({}, taskData));
diff --git a/src/pages/githubAssessments/GitHubClassroom.tsx b/src/pages/githubAssessments/GitHubClassroom.tsx
new file mode 100644
index 0000000000..76fc902c2a
--- /dev/null
+++ b/src/pages/githubAssessments/GitHubClassroom.tsx
@@ -0,0 +1,306 @@
+import { NonIdealState, Spinner } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { Octokit } from '@octokit/rest';
+import { GetResponseDataTypeFromEndpointMethod } from '@octokit/types';
+import * as React from 'react';
+import { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
+
+import { OverallState } from '../../commons/application/ApplicationTypes';
+import ContentDisplay from '../../commons/ContentDisplay';
+import { MissionRepoData } from '../../commons/githubAssessments/GitHubMissionTypes';
+import GitHubAssessmentsNavigationBar from '../../commons/navigationBar/subcomponents/GitHubAssessmentsNavigationBar';
+import { showWarningMessage } from '../../commons/utils/NotificationsHelper';
+import { assessmentTypeLink } from '../../commons/utils/ParamParseHelper';
+import GitHubAssessmentListing from './GitHubAssessmentListing';
+import GitHubAssessmentWorkspaceContainer from './GitHubAssessmentWorkspaceContainer';
+import GitHubClassroomWelcome from './GitHubClassroomWelcome';
+
+type DispatchProps = {
+ handleGitHubLogIn: () => void;
+ handleGitHubLogOut: () => void;
+};
+
+/**
+ * A page that lists the missions available to the authenticated user.
+ * This page should only be reachable if using a GitHub-hosted deployment.
+ */
+const GitHubClassroom: React.FC = props => {
+ const location = useLocation<{
+ courses: string[] | undefined;
+ assessmentTypeOverviews: GHAssessmentTypeOverview[] | undefined;
+ selectedCourse: string | undefined;
+ }>();
+ const octokit: Octokit | undefined = useSelector(
+ (store: OverallState) => store.session.githubOctokitObject
+ ).octokit;
+ const [courses, setCourses] = useState(location.state?.courses);
+ const [selectedCourse, setSelectedCourse] = useState(
+ location.state?.selectedCourse || ''
+ );
+ const [assessmentTypeOverviews, setAssessmentTypeOverviews] = useState<
+ GHAssessmentTypeOverview[] | undefined
+ >(location.state?.assessmentTypeOverviews);
+ const types = assessmentTypeOverviews?.map(overview => overview.typeName);
+
+ useEffect(() => {
+ if (octokit === undefined) {
+ return;
+ }
+
+ if (!courses) {
+ fetchCourses(octokit, setCourses, setSelectedCourse, setAssessmentTypeOverviews);
+ }
+ }, [courses, octokit]);
+
+ const changeCourseHandler = React.useCallback(
+ (e: any) => {
+ if (octokit === undefined) {
+ return;
+ }
+
+ fetchAssessmentOverviews(octokit, e.target.innerText, setAssessmentTypeOverviews);
+ setSelectedCourse(e.target.innerText);
+ },
+ [octokit, setSelectedCourse, setAssessmentTypeOverviews]
+ );
+
+ const refreshAssessmentOverviews = () => {
+ if (octokit === undefined) {
+ return;
+ }
+
+ fetchAssessmentOverviews(octokit, selectedCourse, setAssessmentTypeOverviews);
+ };
+
+ const redirectToLogin = () => ;
+ const redirectToAssessments = () => (
+
+ );
+
+ return (
+
+ {
+ props.handleGitHubLogOut();
+ setCourses(undefined);
+ setAssessmentTypeOverviews(undefined);
+ setSelectedCourse('');
+ }}
+ octokit={octokit}
+ courses={courses}
+ selectedCourse={selectedCourse}
+ types={types}
+ assessmentTypeOverviews={assessmentTypeOverviews}
+ />
+
+ {
+ return octokit && (!courses || (courses.length > 0 && !assessmentTypeOverviews)) ? (
+ } />}
+ loadContentDispatch={() => {}}
+ />
+ ) : octokit && courses && courses.length === 0 ? (
+
+ ) : octokit ? (
+ redirectToAssessments()
+ ) : (
+
+ }
+ loadContentDispatch={() => {}}
+ />
+ );
+ }}
+ />
+ (octokit ? : redirectToLogin())}
+ />
+
+ {octokit
+ ? types?.map((type, idx) => {
+ const filteredAssessments = assessmentTypeOverviews
+ ? assessmentTypeOverviews[idx].assessments
+ : undefined;
+ return (
+ (
+
+ )}
+ key={idx}
+ />
+ );
+ })
+ : null}
+ 0 ? redirectToAssessments : redirectToLogin
+ }
+ />
+
+
+ );
+};
+
+/**
+ * Retrieves list of organizations for the authenticated user.
+ *
+ * @param octokit The Octokit instance for the authenticated user
+ * @param setCourses The React setter function for an array of courses string names
+ * @param setSelectedCourse The React setter function for string name of selected course
+ */
+async function fetchCourses(
+ octokit: Octokit,
+ setCourses: (courses: string[]) => void,
+ setSelectedCourse: (course: string) => void,
+ setAssessmentTypeOverviews: (assessmentTypeOverviews: GHAssessmentTypeOverview[]) => void
+) {
+ const courses: string[] = [];
+ const results = (await octokit.orgs.listForAuthenticatedUser({ per_page: 100 })).data;
+ const orgs = results.filter(org => org.login.includes('source-academy-course')); // filter only organisations with 'source-academy-course' in name
+ orgs.forEach(org => {
+ courses.push(org.login.replace('source-academy-course-', ''));
+ });
+ setCourses(courses);
+ if (courses.length > 0) {
+ setSelectedCourse(courses[0]);
+ fetchAssessmentOverviews(octokit, courses[0], setAssessmentTypeOverviews);
+ }
+}
+
+export type GHAssessmentTypeOverview = {
+ typeName: string;
+ assessments: GHAssessmentOverview[];
+};
+
+export type GHAssessmentOverview = {
+ title: string;
+ coverImage: string;
+ webSummary: string;
+ missionRepoData: MissionRepoData;
+ dueDate: Date;
+ link?: string;
+};
+
+type GitHubAssessment = {
+ id: string;
+ title: string;
+ openAt: string;
+ closeAt: string;
+ published: string;
+ coverImage: string;
+ shortSummary: string;
+ acceptLink: string;
+ repoPrefix: string;
+};
+
+async function fetchAssessmentOverviews(
+ octokit: Octokit,
+ selectedCourse: string,
+ setAssessmentTypeOverviews: (assessmentTypeOverviews: GHAssessmentTypeOverview[]) => void
+) {
+ const userLogin = (await octokit.users.getAuthenticated()).data.login;
+ const orgLogin = 'source-academy-course-'.concat(selectedCourse);
+ type ListForAuthenticatedUserData = GetResponseDataTypeFromEndpointMethod<
+ typeof octokit.repos.listForAuthenticatedUser
+ >;
+ const userRepos: ListForAuthenticatedUserData = (
+ await octokit.repos.listForAuthenticatedUser({ per_page: 100 })
+ ).data;
+ const courseRepos = userRepos.filter(repo => repo.owner!.login === orgLogin);
+ const courseInfoRepo = courseRepos.find(repo => repo.name.includes('course-info'));
+
+ if (courseInfoRepo === undefined) {
+ showWarningMessage('The course-info repository cannot be located.', 2000);
+ return;
+ }
+
+ const files = (
+ await octokit.repos.getContent({
+ owner: courseInfoRepo.owner!.login,
+ repo: courseInfoRepo.name,
+ path: ''
+ })
+ ).data;
+
+ if (Array.isArray(files)) {
+ if (files.find(file => file.name === 'course-info.json')) {
+ const result = await octokit.repos.getContent({
+ owner: courseInfoRepo.owner!.login,
+ repo: courseInfoRepo.name,
+ path: 'course-info.json'
+ });
+
+ const courseInfo = JSON.parse(Buffer.from((result.data as any).content, 'base64').toString());
+
+ courseInfo.types.forEach((type: { typeName: string; assessments: [GitHubAssessment] }) => {
+ const assessmentOverviews: GHAssessmentOverview[] = [];
+ type.assessments.forEach((assessment: GitHubAssessment) => {
+ if (!assessment.published) {
+ return;
+ }
+
+ const prefixLogin = assessment.repoPrefix + '-' + userLogin;
+ const missionRepo = userRepos.find(repo => repo.name === prefixLogin);
+
+ let createdAt = new Date();
+ let acceptLink = undefined;
+ if (missionRepo === undefined) {
+ acceptLink = assessment.acceptLink;
+ } else {
+ if (missionRepo.created_at !== null) {
+ createdAt = new Date(missionRepo.created_at);
+ }
+ }
+
+ assessmentOverviews.push({
+ title: assessment.title,
+ coverImage: assessment.coverImage,
+ webSummary: assessment.shortSummary,
+ missionRepoData: {
+ repoOwner: courseInfoRepo.owner!.login,
+ repoName: prefixLogin,
+ dateOfCreation: createdAt
+ },
+ dueDate: new Date(assessment.closeAt),
+ link: acceptLink
+ });
+
+ assessmentOverviews.sort((a, b) => (a.dueDate <= b.dueDate ? -1 : 1));
+ });
+ (type as any).assessments = assessmentOverviews;
+ });
+ setAssessmentTypeOverviews(courseInfo.types);
+ } else {
+ showWarningMessage('The course-info.json file cannot be located.', 2000);
+ return;
+ }
+ }
+}
+
+export default GitHubClassroom;
diff --git a/src/pages/githubAssessments/GitHubClassroomWelcome.tsx b/src/pages/githubAssessments/GitHubClassroomWelcome.tsx
new file mode 100644
index 0000000000..ab29280fc9
--- /dev/null
+++ b/src/pages/githubAssessments/GitHubClassroomWelcome.tsx
@@ -0,0 +1,34 @@
+import { Card, H2, H4, UL } from '@blueprintjs/core';
+
+const GitHubClassroomWelcome: React.FC = () => {
+ return (
+
+
+
+
+
Welcome to Source Academy with GitHub Classroom!
+
+
Source Academy does not find any course information for this account.
+
+
+
+ If you are enrolled in a Source Academy course that uses GitHub Classroom, check
+ with the course staff to make sure your account is added to the course.
+
+
+ If you are looking for a course to join, check{' '}
+
+ here
+ {' '}
+ to find a course that suits your needs.
+
+
+
+
+
+
+
+ );
+};
+
+export default GitHubClassroomWelcome;
diff --git a/src/pages/githubAssessments/GitHubMissionListing.tsx b/src/pages/githubAssessments/GitHubMissionListing.tsx
deleted file mode 100644
index 7af8eed519..0000000000
--- a/src/pages/githubAssessments/GitHubMissionListing.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-import {
- Button,
- Card,
- Divider,
- Elevation,
- H4,
- H6,
- Icon,
- NonIdealState,
- Spinner,
- SpinnerSize,
- TagInput,
- Text
-} from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
-import { Octokit } from '@octokit/rest';
-import {
- GetResponseDataTypeFromEndpointMethod,
- GetResponseTypeFromEndpointMethod
-} from '@octokit/types';
-import * as React from 'react';
-import { useEffect, useMemo, useState } from 'react';
-import { useSelector } from 'react-redux';
-import { useMediaQuery } from 'react-responsive';
-
-import defaultCoverImage from '../../assets/default_cover_image.jpg';
-import ContentDisplay from '../../commons/ContentDisplay';
-import controlButton from '../../commons/ControlButton';
-import {
- getContentAsString,
- parseMetadataProperties
-} from '../../commons/githubAssessments/GitHubMissionDataUtils';
-import { MissionRepoData } from '../../commons/githubAssessments/GitHubMissionTypes';
-import Markdown from '../../commons/Markdown';
-import Constants from '../../commons/utils/Constants';
-import { history } from '../../commons/utils/HistoryHelper';
-import { getGitHubOctokitInstance } from '../../features/github/GitHubUtils';
-
-type DispatchProps = {
- handleGitHubLogIn: () => void;
- handleGitHubLogOut: () => void;
-};
-
-/**
- * A page that lists the missions available to the authenticated user.
- * This page should only be reachable if using a GitHub-hosted deployment.
- */
-const GitHubMissionListing: React.FC = props => {
- const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
- const octokit: Octokit = useSelector((store: any) => store.session.githubOctokitObject).octokit;
-
- const [display, setDisplay] = useState(<>>);
- const [browsableMissions, setBrowsableMissions] = useState([]);
- const [filterTagNodes, setFilterTagNodes] = useState([]);
- const [filterTagStrings, setFilterTagStrings] = useState([]);
-
- const handleTagChange = React.useCallback((values: React.ReactNode[]) => {
- setFilterTagNodes(values);
-
- const newFilterTagStrings: string[] = [];
- for (let i = 0; i < values.length; i++) {
- const value = values[i];
- if (value) {
- newFilterTagStrings.push(value.toString().toLowerCase());
- }
- }
- setFilterTagStrings(newFilterTagStrings);
- }, []);
-
- const handleTagClear = React.useCallback(() => handleTagChange([]), [handleTagChange]);
-
- const signInToGitHubDisplay = useMemo(
- () => ,
- []
- );
- const noMissionReposFoundDisplay = useMemo(
- () => (
-
- ),
- []
- );
- const createMissionButton = useMemo(
- () => (
- history.push(`/githubassessments/editor`)}>
- Create a New Assignment!
-
- ),
- []
- );
-
- // After browsable missions retrieved, display mission listing
- useEffect(() => {
- if (octokit === undefined) {
- return;
- }
-
- if (browsableMissions.length === 0) {
- setDisplay(
- <>
- {createMissionButton}
- {noMissionReposFoundDisplay}
- >
- );
- return;
- }
-
- // Create tag filter
- const clearButton = ;
- const tagFilter = (
-
- );
-
- // Create cards
- const missionListing =
- filterTagNodes.length === 0
- ? browsableMissions
- : browsableMissions.filter(mission => missionMatchesTags(mission, filterTagStrings));
- const cards = missionListing.map(element => convertMissionToCard(element, isMobileBreakpoint));
-
- setDisplay(
- <>
- {tagFilter}
-
- {createMissionButton}
- {cards}
- {isMobileBreakpoint &&
- controlButton('Log Out', IconNames.GIT_BRANCH, props.handleGitHubLogOut, {
- intent: 'primary',
- minimal: false
- })}
- >
- );
- }, [
- browsableMissions,
- createMissionButton,
- filterTagNodes,
- filterTagStrings,
- handleTagChange,
- handleTagClear,
- isMobileBreakpoint,
- noMissionReposFoundDisplay,
- octokit,
- props.handleGitHubLogOut
- ]);
-
- // Used to retrieve browsable missions
- useEffect(() => {
- if (octokit === undefined) {
- setDisplay(
- <>
- {signInToGitHubDisplay}
- {isMobileBreakpoint &&
- controlButton('Log In', IconNames.GIT_BRANCH, props.handleGitHubLogIn, {
- intent: 'primary',
- minimal: false
- })}
- >
- );
- } else {
- retrieveBrowsableMissions(octokit, setBrowsableMissions, setDisplay);
- }
- }, [
- isMobileBreakpoint,
- octokit,
- props.handleGitHubLogIn,
- setBrowsableMissions,
- setDisplay,
- signInToGitHubDisplay
- ]);
-
- return (
-
- );
-};
-
-function missionMatchesTags(mission: BrowsableMission, tags: string[]) {
- let match = false;
-
- for (let i = 0; i < tags.length; i++) {
- const tag = tags[i];
-
- if (tag) {
- const titleIncludesTag = mission.title.toLowerCase().includes(tag);
- const summaryIncludesTag = mission.webSummary.toLowerCase().includes(tag);
- const ownerLoginIncludesTag = mission.missionRepoData.repoOwner.toLowerCase().includes(tag);
- match = titleIncludesTag || summaryIncludesTag || ownerLoginIncludesTag;
- }
-
- if (match) {
- break;
- }
- }
- return match;
-}
-
-/**
- * Retrieves BrowsableMissions information from a mission repositories and sets them in the page's state.
- *
- * @param octokit The Octokit instance for the authenticated user
- * @param setBrowsableMissions The React setter function for an array of BrowsableMissions
- * @param setDisplay The React setter function for the page's display
- */
-async function retrieveBrowsableMissions(
- octokit: Octokit,
- setBrowsableMissions: (browsableMissions: BrowsableMission[]) => void,
- setDisplay: (display: JSX.Element) => void
-) {
- if (octokit === undefined) return;
-
- setDisplay( } />);
-
- type ListForAuthenticatedUserData = GetResponseDataTypeFromEndpointMethod<
- typeof octokit.repos.listForAuthenticatedUser
- >;
- const allRepos: ListForAuthenticatedUserData = (
- await octokit.repos.listForAuthenticatedUser({ per_page: 100 })
- ).data;
- const correctlyNamedRepos = allRepos.filter((repo: any) => repo.name.startsWith('sa-'));
-
- const getContentPromises = correctlyNamedRepos.map(repo => {
- const login = (repo.owner as any).login;
- const repoName = repo.name;
- const createdAt = repo.created_at;
-
- const promiseCreator = async () => {
- const getContentResponse = await octokit.repos.getContent({
- owner: login,
- repo: repo.name,
- path: ''
- });
-
- return {
- getContentResponse: getContentResponse,
- login: login,
- repoName: repoName,
- createdAt: createdAt
- };
- };
-
- return promiseCreator();
- });
-
- const foundMissionRepos: MissionRepoData[] = [];
- let unreachableCodeReached = false;
-
- Promise.all(getContentPromises).then((promisedContents: any[]) => {
- promisedContents.forEach((promisedContent: any) => {
- type GetContentData = GetResponseDataTypeFromEndpointMethod;
- type GetContentResponse = GetResponseTypeFromEndpointMethod;
-
- const getContentResponse: GetContentResponse = promisedContent.getContentResponse;
- const files: GetContentData = getContentResponse.data;
- const login: string = promisedContent.login;
- const repoName: string = promisedContent.repoName;
- const createdAt: string = promisedContent.createdAt;
-
- if (!Array.isArray(files)) {
- // Code should not reach this point
- unreachableCodeReached = true;
- return;
- }
-
- const githubSubDirectories = files as any[];
-
- let repositoryContainsMetadataFile = false;
- for (let j = 0; j < githubSubDirectories.length; j++) {
- const file = githubSubDirectories[j];
- if (file.name === '.metadata') {
- repositoryContainsMetadataFile = true;
- break;
- }
- }
-
- if (repositoryContainsMetadataFile) {
- const missionRepoData: MissionRepoData = {
- repoOwner: login,
- repoName: repoName,
- dateOfCreation: new Date(createdAt)
- };
- foundMissionRepos.push(missionRepoData);
- }
- });
-
- if (unreachableCodeReached) {
- return;
- }
-
- const missionPromises = foundMissionRepos.map(missionRepoData =>
- convertRepoToBrowsableMission(missionRepoData, octokit)
- );
- Promise.all(missionPromises).then((browsableMissions: BrowsableMission[]) => {
- browsableMissions.sort((a, b) => {
- return a.missionRepoData.dateOfCreation < b.missionRepoData.dateOfCreation ? 1 : -1;
- });
- setBrowsableMissions(browsableMissions);
- });
- });
-
- if (unreachableCodeReached) {
- setDisplay(
-
- );
- return;
- }
-}
-
-async function convertRepoToBrowsableMission(missionRepo: MissionRepoData, octokit: Octokit) {
- const metadata = await getContentAsString(
- missionRepo.repoOwner,
- missionRepo.repoName,
- '.metadata',
- octokit
- );
- const browsableMission = createBrowsableMission(missionRepo, metadata);
-
- return browsableMission;
-}
-
-type BrowsableMission = {
- title: string;
- coverImage: string;
- webSummary: string;
- missionRepoData: MissionRepoData;
- dueDate: Date;
-};
-
-/**
- * Maps from a MissionRepoData to a BrowsableMission.
- *
- * @param missionRepo Repository information for a mission repository
- * @param metadata The contents of the '.metadata' file for the same mission repository
- */
-function createBrowsableMission(missionRepo: MissionRepoData, metadata: string) {
- const browsableMission: BrowsableMission = {
- title: '',
- coverImage: '',
- webSummary: '',
- missionRepoData: {
- repoOwner: '',
- repoName: '',
- dateOfCreation: new Date(8640000000000000)
- },
- dueDate: new Date(8640000000000000)
- };
-
- browsableMission.missionRepoData = missionRepo;
-
- const stringProps = ['coverImage', 'title', 'webSummary'];
- const dateProps = ['dueDate'];
-
- const retVal = parseMetadataProperties(
- browsableMission,
- stringProps,
- [],
- dateProps,
- metadata
- );
-
- return retVal;
-}
-
-/**
- * Maps from a BrowsableMission object to a JSX card that can be displayed on the Mission Listing.
- *
- * @param missionRepo The BrowsableMission representation of a single mission repository
- * @param isMobileBreakpoint Whether we are using mobile breakpoint
- */
-function convertMissionToCard(missionRepo: BrowsableMission, isMobileBreakpoint: boolean) {
- const ratio = isMobileBreakpoint ? 5 : 3;
- const ownerSlashName =
- missionRepo.missionRepoData.repoOwner + '/' + missionRepo.missionRepoData.repoName;
- const dueDate = missionRepo.dueDate.toDateString();
-
- const hasDueDate = new Date(8640000000000000) > missionRepo.dueDate;
- const isOverdue = new Date() > missionRepo.dueDate;
- const buttonText = isOverdue ? 'Review Answers' : 'Open';
-
- const data = missionRepo.missionRepoData;
-
- const loadIntoEditor = () => history.push(`/githubassessments/editor`, data);
-
- return (
-
-
-
-
-
-
-
-
-
- {missionRepo.title}
- {ownerSlashName}
-
-
-
-
-
-
-
-
-
-
- {hasDueDate ? 'Due: ' + dueDate : 'No due date'}
-
-
-
- {buttonText}
-
-
-
-
-
-
- );
-}
-
-export default GitHubMissionListing;
diff --git a/src/pages/githubAssessments/__tests__/GitHubClassroom.tsx b/src/pages/githubAssessments/__tests__/GitHubClassroom.tsx
new file mode 100644
index 0000000000..dce137593b
--- /dev/null
+++ b/src/pages/githubAssessments/__tests__/GitHubClassroom.tsx
@@ -0,0 +1,126 @@
+import { act } from '@testing-library/react';
+import { shallow } from 'enzyme';
+import { useSelector } from 'react-redux';
+
+import GitHubClassroom from '../GitHubClassroom';
+
+type objectPerPage = {
+ per_page: number;
+};
+
+async function listOrgsForAuthenticatedUser(orgProp: objectPerPage) {
+ return {
+ data: [
+ { login: 'source-academy-course-test' },
+ { login: 'not' },
+ { login: 'source-academy-course-second' },
+ { login: 'valid' },
+ { login: 'source-academy-course-third' }
+ ]
+ };
+}
+
+async function listReposForAuthenticatedUser(repoProp: objectPerPage) {
+ return {
+ data: [
+ { name: 'course-info', owner: { login: 'source-academy-course-test' } },
+ { name: 'sa-test-mission', owner: { login: 'source-academy-course-test' } },
+ { name: 'sa-demo-mission', owner: { login: 'source-academy-course-test' } },
+ { name: 'sa-autotester-test', owner: { login: 'source-academy-course-test' } }
+ ]
+ };
+}
+
+const mockCourseInfo =
+ 'ewogICJDb3Vyc2VOYW1lIjogIkNTMTEwMVMiLAogICJ0eXBlcyI6CiAgWwogICAgewogICAgICAidHlwZU5hbWUiOiAiTm90TWlzc2lvbnMiLAogICAgICAiYXNzZXNzbWVudHMiOgogICAgICBbCiAgICAgICAgewogICAgICAgICAgImlkIjogIjEiLAogICAgICAgICAgInRpdGxlIjogIkN1cnZlIEludHJvZHVjdGlvbiIsCiAgICAgICAgICAib3BlbkF0IjogIjIwMjAtMTItMDFUMDA6MDA6MDArMDg6MDAiLAogICAgICAgICAgImNsb3NlQXQiOiAiMjAyMS0xMi0zMVQyMzo1OTo1OSswODowMCIsCiAgICAgICAgICAicHVibGlzaGVkIjogInllcyIsCiAgICAgICAgICAiY292ZXJJbWFnZSI6ICJodHRwczovL2kuaW1ndXIuY29tL3EyTzRpd2EucG5nIiwKICAgICAgICAgICJzaG9ydFN1bW1hcnkiOiAiSW4gdGhpcyBtaXNzaW9uLCB5b3UgZ2V0IGludHJvZHVjZWQgdG8gdmlzaWJsZSBmdW5jdGlvbnMsIGNhbGxlZCBDdXJ2ZXMhIiwKICAgICAgICAgICJhY2NlcHRMaW5rIjogImh0dHBzOi8vY2xhc3Nyb29tLmdpdGh1Yi5jb20vYS9QeUFVaGRmZSIsCiAgICAgICAgICAicmVwb1ByZWZpeCI6ICJzYS10ZXN0LW1pc3Npb24iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiMiIsCiAgICAgICAgICAidGl0bGUiOiAiRGVtbyBNaXNzaW9uIiwKICAgICAgICAgICJvcGVuQXQiOiAiMjAyMC0xMi0wMVQwMDowMDowMCswODowMCIsCiAgICAgICAgICAiY2xvc2VBdCI6ICIyMDIxLTEyLTMxVDIzOjU5OjU5KzA4OjAwIiwKICAgICAgICAgICJwdWJsaXNoZWQiOiAieWVzIiwKICAgICAgICAgICJjb3ZlckltYWdlIjogImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS8zNTYyMDcwNT9zPTQwMCZ1PTMyZjcyZmQxZDY1YTBkNjg3N2FkMWQ1ODcwZmZhMzI3ZGRhNzU0ZjEmdj00IiwKICAgICAgICAgICJzaG9ydFN1bW1hcnkiOiAiUXVpY2tzb3J0IGFzc2lnbm1lbnQgZGVzY3JpcHRpb24hIiwKICAgICAgICAgICJhY2NlcHRMaW5rIjogImh0dHBzOi8vY2xhc3Nyb29tLmdpdGh1Yi5jb20vYS9DeGxxakxhUCIsCiAgICAgICAgICAicmVwb1ByZWZpeCI6ICJzYS1kZW1vLW1pc3Npb24iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiaWQiOiAiMyIsCiAgICAgICAgICAidGl0bGUiOiAiU29ydGluZyBUaGluZ3MgT3V0IiwKICAgICAgICAgICJvcGVuQXQiOiAiMjAyMC0xMi0wMVQwMDowMDowMCswODowMCIsCiAgICAgICAgICAiY2xvc2VBdCI6ICIyMDIxLTEyLTMxVDIzOjU5OjU5KzA4OjAwIiwKICAgICAgICAgICJwdWJsaXNoZWQiOiAieWVzIiwKICAgICAgICAgICJjb3ZlckltYWdlIjogIi8vczMtYXAtc291dGhlYXN0LTEuYW1hem9uYXdzLmNvbS9taXNzaW9uLWFzc2V0cy9taXNzaW9ucy9RdWlja3NvcnQucG5nIiwKICAgICAgICAgICJzaG9ydFN1bW1hcnkiOiAiQSBxdWljayBsb29rIGF0IHF1aWNrc29ydC4iLAogICAgICAgICAgImFjY2VwdExpbmsiOiAiaHR0cHM6Ly9jbGFzc3Jvb20uZ2l0aHViLmNvbS9hL0QxNmhXdmpBIiwKICAgICAgICAgICJyZXBvUHJlZml4IjogInNhLWF1dG90ZXN0ZXItdGVzdCIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICB7CiAgICAgICJ0eXBlTmFtZSI6ICJOb3RRdWVzdHMiLAogICAgICAiYXNzZXNzbWVudHMiOgogICAgICBbCiAgICAgICAgewogICAgICAgICAgImlkIjogIjQiLAogICAgICAgICAgInRpdGxlIjogIkZha2UgUXVlc3QiLAogICAgICAgICAgIm9wZW5BdCI6ICIyMDIwLTEyLTAxVDAwOjAwOjAwKzA4OjAwIiwKICAgICAgICAgICJjbG9zZUF0IjogIjIwMjEtMTItMzFUMjM6NTk6NTkrMDg6MDAiLAogICAgICAgICAgInB1Ymxpc2hlZCI6ICJ5ZXMiLAogICAgICAgICAgImNvdmVySW1hZ2UiOiAiaHR0cHM6Ly9pLmt5bS1jZG4uY29tL2VudHJpZXMvaWNvbnMvZmFjZWJvb2svMDAwLzAzNy8wMzcvcGFpbnBla29jb3Zlci5qcGciLAogICAgICAgICAgInNob3J0U3VtbWFyeSI6ICJBIGZha2UgcXVlc3QgdGhhdCBzaG91bGQgc2hvdyB1cC4iLAogICAgICAgICAgImFjY2VwdExpbmsiOiAiaHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1NNVZfSVhNZXdsNCIsCiAgICAgICAgICAicmVwb1ByZWZpeCI6ICJzYS10aGlzLWRvZXMtbm90LWV4aXN0IgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgIHsKICAgICAgInR5cGVOYW1lIjoiTm90UGF0aHMiLAogICAgICAiYXNzZXNzbWVudHMiOltdCiAgICB9LAogICAgewogICAgICAidHlwZU5hbWUiOiJOb3RDb250ZXN0cyIsCiAgICAgICJhc3Nlc3NtZW50cyI6W10KICAgIH0sCiAgICB7CiAgICAgICJ0eXBlTmFtZSI6Ik90aGVycyIsCiAgICAgICJhc3Nlc3NtZW50cyI6W10KICAgIH0KICBdCn0=';
+
+type getContentProp = {
+ owner: string;
+ repo: string;
+ path: string;
+};
+
+async function getContent(getContentProp: getContentProp) {
+ const owner = getContentProp.owner;
+ const repo = getContentProp.repo;
+ const path = getContentProp.path;
+ if (owner === 'source-academy-course-test' && repo === 'course-info') {
+ if (path === '' || path === undefined) {
+ return {
+ data: [{ name: 'course-info.json' }]
+ };
+ }
+ if (path === 'course-info.json') {
+ return {
+ data: {
+ content: mockCourseInfo
+ }
+ };
+ }
+ }
+ return {
+ data: [{ name: 'not' }, { name: 'important' }]
+ };
+}
+
+async function getAuthenticated() {
+ return {
+ data: {
+ login: 'Fubuki'
+ }
+ };
+}
+
+const mockStore = {
+ session: {
+ githubOctokitObject: {
+ octokit: {
+ orgs: {
+ listForAuthenticatedUser: listOrgsForAuthenticatedUser
+ },
+ repos: {
+ listForAuthenticatedUser: listReposForAuthenticatedUser,
+ getContent: getContent
+ },
+ users: {
+ getAuthenticated: getAuthenticated
+ }
+ }
+ }
+ }
+};
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn()
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: () => ({
+ state: undefined
+ }),
+ useParams: () => ({
+ selectedType: undefined
+ })
+}));
+
+describe('GitHubClassroom', () => {
+ beforeEach(() => {
+ (useSelector as jest.Mock).mockImplementation(callback => {
+ return callback(mockStore);
+ });
+ });
+
+ const mockProps = {
+ handleGitHubLogIn: () => {},
+ handleGitHubLogOut: () => {}
+ };
+
+ it('renders correctly', async () => {
+ await act(async () => {
+ const tree = shallow( );
+ expect(tree.debug()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/pages/githubAssessments/__tests__/__snapshots__/GitHubClassroom.tsx.snap b/src/pages/githubAssessments/__tests__/__snapshots__/GitHubClassroom.tsx.snap
new file mode 100644
index 0000000000..009f12b34d
--- /dev/null
+++ b/src/pages/githubAssessments/__tests__/__snapshots__/GitHubClassroom.tsx.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GitHubClassroom renders correctly 1`] = `
+"
+
+
+
+
+
+
+
+
"
+`;
diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx
index 86700464d0..52ebdd2a41 100644
--- a/src/pages/playground/Playground.tsx
+++ b/src/pages/playground/Playground.tsx
@@ -40,12 +40,11 @@ import MobileWorkspace, {
import SideContentDataVisualizer from '../../commons/sideContent/SideContentDataVisualizer';
import SideContentEnvVisualizer from '../../commons/sideContent/SideContentEnvVisualizer';
import SideContentFaceapiDisplay from '../../commons/sideContent/SideContentFaceapiDisplay';
-import SideContentInspector from '../../commons/sideContent/SideContentInspector';
import SideContentRemoteExecution from '../../commons/sideContent/SideContentRemoteExecution';
import SideContentSubstVisualizer from '../../commons/sideContent/SideContentSubstVisualizer';
import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes';
import SideContentVideoDisplay from '../../commons/sideContent/SideContentVideoDisplay';
-import Constants from '../../commons/utils/Constants';
+import Constants, { Links } from '../../commons/utils/Constants';
import { generateSourceIntroduction } from '../../commons/utils/IntroductionHelper';
import { stringParamToInt } from '../../commons/utils/ParamParseHelper';
import { parseQuery } from '../../commons/utils/QueryHelper';
@@ -65,6 +64,7 @@ export type OwnProps = {
isSicpEditor?: boolean;
initialEditorValueHash?: string;
initialPrependHash?: string | undefined;
+ initialFullProgramHash?: string;
handleCloseEditor?: () => void;
};
@@ -552,25 +552,30 @@ const Playground: React.FC = props => {
/>
);
- const shareButton = React.useMemo(
- () => (
+ const shareButton = React.useMemo(() => {
+ const queryString = isSicpEditor
+ ? Links.playground + '#' + props.initialFullProgramHash
+ : props.queryString;
+ return (
- ),
- [
- props.handleGenerateLz,
- props.handleShortenURL,
- props.handleUpdateShortURL,
- props.queryString,
- props.shortURL
- ]
- );
+ );
+ }, [
+ isSicpEditor,
+ props.handleGenerateLz,
+ props.handleShortenURL,
+ props.handleUpdateShortURL,
+ props.initialFullProgramHash,
+ props.queryString,
+ props.shortURL
+ ]);
const playgroundIntroductionTab: SideContentTab = React.useMemo(
() => ({
@@ -618,8 +623,7 @@ const Playground: React.FC = props => {
props.sourceVariant !== 'non-det' &&
!usingRemoteExecution
) {
- // Enable Inspector, Env Visualizer for Source Chapter 3 and above
- tabs.push(inspectorTab);
+ // Enable Env Visualizer for Source Chapter 3 and above
tabs.push(envVisualizerTab);
}
@@ -854,14 +858,6 @@ const FaceapiDisplayTab: SideContentTab = {
toSpawn: () => true
};
-const inspectorTab: SideContentTab = {
- label: 'Inspector',
- iconName: IconNames.SEARCH,
- body: ,
- id: SideContentType.inspector,
- toSpawn: () => true
-};
-
const envVisualizerTab: SideContentTab = {
label: 'Env Visualizer',
iconName: IconNames.GLOBE,
diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx
index b3abcc199a..572cf6fee0 100644
--- a/src/pages/sicp/Sicp.tsx
+++ b/src/pages/sicp/Sicp.tsx
@@ -1,15 +1,17 @@
import 'katex/dist/katex.min.css';
-import { Classes, NonIdealState, Spinner } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
+import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core';
import classNames from 'classnames';
import * as React from 'react';
import { useDispatch } from 'react-redux';
-import { RouteComponentProps, useParams } from 'react-router';
+import { RouteComponentProps, useHistory, useParams } from 'react-router';
import Constants from 'src/commons/utils/Constants';
import { resetWorkspace, toggleUsingSubst } from 'src/commons/workspace/WorkspaceActions';
import { parseArr, ParseJsonError } from 'src/features/sicp/parser/ParseJson';
+import { getNext, getPrev } from 'src/features/sicp/TableOfContentsHelper';
+import SicpErrorBoundary from '../../features/sicp/errors/SicpErrorBoundary';
+import getSicpError, { SicpErrorType } from '../../features/sicp/errors/SicpErrors';
import SicpIndexPage from './subcomponents/SicpIndexPage';
type SicpProps = RouteComponentProps<{}>;
@@ -25,47 +27,29 @@ export const CodeSnippetContext = React.createContext({
const loadingComponent = } />;
-const unexpectedError = (
-
-);
-const pageNotFoundError = (
-
-);
-const parsingError = (
-
-);
-
-const errorComponent = (description: JSX.Element) => (
-
-);
-
const Sicp: React.FC = props => {
const [data, setData] = React.useState(<>>);
const [loading, setLoading] = React.useState(true);
const [active, setActive] = React.useState('0');
const { section } = useParams<{ section: string }>();
const topRef = React.useRef(null);
+ const bottomRef = React.useRef(null);
const refs = React.useRef({});
+ const history = useHistory();
+
+ const scrollRefIntoView = (ref: HTMLDivElement | null) => {
+ if (!ref) {
+ return;
+ }
+
+ // Hack to get scrolling to work properly.
+ // When 'block: start' option is used with scrollIntoView, the whole page scrolls with it.
+ // This issue does not occur when the option 'block: nearest' is used.
+ // To get `block: nearest` to mimic `block: start` behaviour, we first scroll to the bottom of
+ // the page before scrolling to the desired ref using the `block: nearest` option.
+ bottomRef.current!.scrollIntoView({ block: 'end' });
+ ref.scrollIntoView({ block: 'nearest' });
+ };
// Fetch json data
React.useEffect(() => {
@@ -93,38 +77,33 @@ const Sicp: React.FC = props => {
}
})
.catch(error => {
- console.log(error);
+ console.error(error);
if (error.message === 'Not Found') {
// page not found
- setData(errorComponent(pageNotFoundError));
+ setData(getSicpError(SicpErrorType.PAGE_NOT_FOUND_ERROR));
} else if (error instanceof ParseJsonError) {
// error occured while parsing JSON
- setData(errorComponent(parsingError));
+ setData(getSicpError(SicpErrorType.PARSING_ERROR));
} else {
- setData(errorComponent(unexpectedError));
+ setData(getSicpError(SicpErrorType.UNEXPECTED_ERROR));
}
+
setLoading(false);
});
}, [section]);
// Scroll to correct position
- React.useLayoutEffect(() => {
- const hash = props.location.hash;
-
- if (!hash) {
- if (topRef.current) {
- topRef.current.scrollIntoView();
- }
+ React.useEffect(() => {
+ if (loading) {
return;
}
+ const hash = props.location.hash;
const ref = refs.current[hash];
- if (ref) {
- ref.scrollIntoView({ block: 'start' });
- }
- }, [props.location.hash, data]);
+ scrollRefIntoView(ref);
+ }, [props.location.hash, loading]);
// Close all active code snippet when new page is loaded
React.useEffect(() => {
@@ -137,19 +116,37 @@ const Sicp: React.FC = props => {
dispatch(resetWorkspace('sicp'));
dispatch(toggleUsingSubst(false, 'sicp'));
};
+ const handleNavigation = (sect: string | undefined) => {
+ history.push('/sicpjs/' + sect);
+ };
+
+ const navigationButtons = (
+
+ {getPrev(section) && (
+ handleNavigation(getPrev(section))}>Previous
+ )}
+ {getNext(section) && handleNavigation(getNext(section))}>Next }
+
+ );
return (
-
-
- {loading ? (
- {loadingComponent}
- ) : section === 'index' ? (
-
- ) : (
- {data}
- )}
-
+
+
+
+ {loading ? (
+ {loadingComponent}
+ ) : section === 'index' ? (
+
+ ) : (
+
+ {data}
+ {navigationButtons}
+
+ )}
+
+
+
);
};
diff --git a/src/pages/sicp/subcomponents/CodeSnippet.tsx b/src/pages/sicp/subcomponents/CodeSnippet.tsx
index 9bfc01e147..a4ad6b15a7 100644
--- a/src/pages/sicp/subcomponents/CodeSnippet.tsx
+++ b/src/pages/sicp/subcomponents/CodeSnippet.tsx
@@ -19,8 +19,8 @@ type OwnProps = {
output: string;
id: string;
initialEditorValueHash: string;
- initialPrependHash?: string | undefined;
- initialFullProgramHash?: string | undefined;
+ initialPrependHash: string | undefined;
+ initialFullProgramHash: string | undefined;
};
const resizableProps = {
@@ -36,7 +36,7 @@ const resizableProps = {
},
defaultSize: {
width: '100%',
- height: '400px'
+ height: '500px'
},
minHeight: '250px',
maxHeight: '2000px'
@@ -65,6 +65,7 @@ const CodeSnippet: React.FC = props => {
? props.initialFullProgramHash
: props.initialEditorValueHash,
initialPrependHash: showPrepend ? undefined : props.initialPrependHash,
+ initialFullProgramHash: props.initialFullProgramHash,
isSicpEditor: true,
handleCloseEditor: handleClose
@@ -105,11 +106,13 @@ const CodeSnippet: React.FC = props => {
) : (
-
-
-
-
-
+
)}
) : (
diff --git a/src/pages/sicp/subcomponents/SicpToc.tsx b/src/pages/sicp/subcomponents/SicpToc.tsx
index 7664b25da7..ac1b8c9025 100644
--- a/src/pages/sicp/subcomponents/SicpToc.tsx
+++ b/src/pages/sicp/subcomponents/SicpToc.tsx
@@ -2,7 +2,7 @@ import { Tree, TreeNodeInfo } from '@blueprintjs/core';
import { cloneDeep } from 'lodash';
import * as React from 'react';
import { useState } from 'react';
-import { Redirect } from 'react-router';
+import { useHistory } from 'react-router';
import toc from '../../../features/sicp/data/toc.json';
@@ -17,7 +17,7 @@ type OwnProps = {
*/
const SicpToc: React.FC = props => {
const [sidebarContent, setSidebarContent] = useState(toc as TreeNodeInfo[]);
- const [slug, setSlug] = useState('');
+ const history = useHistory();
const handleNodeExpand = (_node: TreeNodeInfo, path: integer[]) => {
const newState = cloneDeep(sidebarContent);
@@ -36,14 +36,13 @@ const SicpToc: React.FC = props => {
if (props.handleCloseToc) {
props.handleCloseToc();
}
- setSlug(String(node.nodeData));
+ history.push('/sicpjs/' + String(node.nodeData));
},
- [props]
+ [history, props]
);
return (
- {slug !== '' &&
}
= props => {
toSpawn: () => true
},
dataVisualizerTab,
- inspectorTab,
envVisualizerTab
];
@@ -389,14 +387,6 @@ const dataVisualizerTab: SideContentTab = {
toSpawn: () => true
};
-const inspectorTab: SideContentTab = {
- label: 'Inspector',
- iconName: IconNames.SEARCH,
- body: ,
- id: SideContentType.inspector,
- toSpawn: () => true
-};
-
const envVisualizerTab: SideContentTab = {
label: 'Env Visualizer',
iconName: IconNames.GLOBE,
diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss
index d9ee2e2c65..88fb031b6d 100644
--- a/src/styles/_academy.scss
+++ b/src/styles/_academy.scss
@@ -120,6 +120,7 @@
.listing-text {
padding: 0 0 0 0.5rem;
+ justify-content: space-between;
.#{$ns}-heading {
margin: 0;
diff --git a/src/styles/_commons.scss b/src/styles/_commons.scss
index 8929af97ea..b35195cf0a 100644
--- a/src/styles/_commons.scss
+++ b/src/styles/_commons.scss
@@ -1,5 +1,5 @@
.ContentDisplay {
- height: 100%;
+ height: fit-content;
width: 100%;
&.row {
diff --git a/src/styles/_contributors.scss b/src/styles/_contributors.scss
index 0ab6d3e97d..f2f7e34e54 100644
--- a/src/styles/_contributors.scss
+++ b/src/styles/_contributors.scss
@@ -37,7 +37,6 @@
padding: 1% 1% 1% 1%;
h3 {
- text-transform: capitalize;
font-weight: bold;
font-style: oblique;
}
@@ -61,10 +60,12 @@
vertical-align: top;
display: inline-block;
width: 120px;
-
&.wider {
width: 140px;
}
+ &.evenWider {
+ width: 180px;
+ }
}
}
diff --git a/src/styles/_github.scss b/src/styles/_github.scss
index 98b5bb7f1b..9aedebe36c 100644
--- a/src/styles/_github.scss
+++ b/src/styles/_github.scss
@@ -41,3 +41,19 @@
width: auto;
overflow-y: scroll;
}
+
+.github-welcome {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ text-align: center;
+
+ .github-welcome-content {
+ padding: 10px 20px 10px 20px;
+ display: inline-block;
+ margin: 0 0 10px 0;
+ width: 80%;
+ @include mQ(750px) {
+ width: 90%;
+ }
+ }
+}
diff --git a/src/styles/_githubAssessments.scss b/src/styles/_githubAssessments.scss
index d77a6ce89d..0f6a7c3425 100644
--- a/src/styles/_githubAssessments.scss
+++ b/src/styles/_githubAssessments.scss
@@ -11,180 +11,6 @@
flex: 1 1 100%;
}
-// This is mostly a copy of 'Assessment' style in _academy.scss
-// Scrollbar is added to fit better within the dialog
-.MissionBrowserContent {
- color: $cadet-color-3;
-
- @media only screen and (max-width: 768px) {
- // for mobile display
- .ContentDisplay {
- .contentdisplay-content.#{$ns}-card {
- padding: 10px;
- }
-
- .listing {
- height: 160px;
- }
-
- .listing-picture {
- height: 100%;
- padding: 0;
- }
-
- .listing-text {
- padding: 0 0 0 0.5rem;
- justify-content: space-between;
-
- .#{$ns}-heading {
- margin: 0;
- }
- .listing-header {
- margin-bottom: 0;
- }
-
- .listing-description {
- max-height: 52px;
- overflow-y: auto;
- font-size: 12px;
- margin: 0.5rem 0;
- .#{$ns}-running-text {
- font-size: 12px;
- }
- }
-
- .listing-footer {
- font-size: 12px;
- }
- }
- }
- }
-
- .contentdisplay-content.#{$ns}-card {
- padding: 10px 20px 10px 20px;
-
- button.collapse-button {
- /* To override the center-xs center alignment */
- display: block;
- margin: 0 0 10px 0;
- }
- }
-
- .listing.#{$ns}-card {
- margin: 0 0 1rem 0;
- }
-
- .listing {
- background-color: $cadet-color-5;
- margin: 0px;
- padding: 0;
- text-align: justify;
-
- & > * {
- overflow-wrap: break-word;
- }
- }
-
- .listing-picture {
- padding: 0;
- position: relative;
-
- img {
- height: 100%;
- width: 100%;
- object-fit: cover;
- border-radius: 3px 0 0 3px;
- }
-
- img.cover-image-submitted {
- filter: gray; /* IE6-9 */
- -webkit-filter: grayscale(1); /* Google Chrome, Safari 6+ & Opera 15+ */
- filter: grayscale(1); /* Microsoft Edge and Firefox 35+ */
- }
-
- /* Disable grayscale on hover */
- img.cover-image-submitted:hover {
- -webkit-filter: grayscale(0);
- filter: none;
- }
- }
-
- .listing-text {
- padding: 0.5rem 0.5rem 0.5rem 1rem;
- border: 1rem;
- display: flex;
- flex-direction: column;
- }
-
- .listing-header {
- margin-bottom: 0.8rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .listing-title {
- margin-bottom: 0;
-
- h4 {
- margin-top: 4px;
- }
- }
-
- .listing-title-tooltip {
- /* Space out icon tooltips */
- margin-left: 2px;
-
- /* Visually separate first icon tooltip from assessment title */
- &:first-of-type {
- margin-left: 6px;
- }
-
- .#{$ns}-icon {
- vertical-align: baseline;
- }
- }
- }
-
- .listing-description {
- flex-grow: 1;
- flex-shrink: 0;
- /* Creates space between the description text, buttons and title */
- margin: 0.5rem 0rem 0.5rem 0.5rem;
-
- & > * {
- /* Limit height of description on smaller screens */
- max-height: 30vh;
- /* Add padding to visually separate scrollbar from content */
- padding-right: 0.5rem;
- overflow-y: auto;
- }
- }
-
- .listing-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .listing-due-date {
- display: flex;
- overflow-x: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- align-items: center;
- }
-
- .listing-due-icon {
- margin-right: 0.4rem;
- }
- }
-
- .listing-button {
- /* Prevent container collapsing and forcing button text to two lines */
- flex-grow: 0;
- flex-shrink: 0;
- }
-}
-
.SideContentMissionEditor {
height: 100%;
display: flex;
diff --git a/src/styles/_sicp.scss b/src/styles/_sicp.scss
index 972e33ce13..9ba2c3f7b4 100644
--- a/src/styles/_sicp.scss
+++ b/src/styles/_sicp.scss
@@ -1,9 +1,18 @@
$sicp-text-color: #333333;
$sicp-background-color: #ffffff;
+$sicp-max-width: 1050px;
+$sicp-code-snippet-width: 90vw;
+$sicp-code-snippet-max-width: 1500px;
+$sicp-content-lr-padding: 6em;
+
+// Override Sass min()
+@function min($numbers...) {
+ @return m#{i}n(#{$numbers});
+}
+
.Sicp {
width: 100%;
- text-align: justify;
color: $sicp-text-color;
overflow: auto;
background-color: $sicp-background-color;
@@ -14,7 +23,10 @@ $sicp-background-color: #ffffff;
font-size: 1.2em;
}
- mjx-container,
+ .bp3-non-ideal-state {
+ font-size: 1rem;
+ }
+
pre {
overflow-x: scroll;
}
@@ -34,20 +46,40 @@ $sicp-background-color: #ffffff;
}
.bp3-code-block {
+ font-size: 0.9em;
+ padding: 0 15px;
+ margin: 0;
+ box-shadow: none;
color: $sicp-text-color;
background-color: $sicp-background-color;
}
+ .katex {
+ font-size: 1em;
+ }
+
.sicp-content {
margin: 1em auto;
- padding: 0 6em;
- max-width: 1050px;
-
+ padding: 0 $sicp-content-lr-padding;
+ max-width: $sicp-max-width;
height: fit-content;
background-color: $sicp-background-color;
- h1 {
- text-align: left;
+ .sicp-navigation-buttons {
+ margin: 25px 0;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+
+ .bp3-button {
+ background-color: $cadet-color-3;
+ width: 80px;
+ padding: 10px 15px;
+ }
+
+ .bp3-button:hover {
+ background-color: $cadet-color-1;
+ }
}
p {
@@ -112,18 +144,6 @@ $sicp-background-color: #ffffff;
}
}
- .sicp-toc-tree {
- .bp3-tree-node-content {
- height: fit-content;
- padding: 3px 0;
-
- .bp3-tree-node-label {
- white-space: normal;
- text-overflow: clip;
- }
- }
- }
-
@media only screen and (max-width: 768px) {
h1,
h2,
@@ -147,17 +167,42 @@ $sicp-background-color: #ffffff;
.sicp-code-snippet {
margin: 10px 0;
+ line-height: 1;
.sicp-code-snippet-open {
+ width: 100vw;
+ margin: 25px 0;
+ transform: translateX(
+ min(
+ -$sicp-content-lr-padding,
+ calc(#{$sicp-max-width} / 2 - 50vw - #{$sicp-content-lr-padding})
+ )
+ );
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+
> .ControlBar {
display: flex;
background-color: $cadet-color-1;
color: white;
padding: 5px;
+ width: $sicp-code-snippet-width;
+ max-width: $sicp-code-snippet-max-width;
.ControlBar_flow {
flex-grow: 1;
}
+
+ @media only screen and (max-width: 768px) {
+ width: 100%;
+ max-width: unset;
+ }
+ }
+
+ .sicp-code-snippet-desktop-open {
+ width: $sicp-code-snippet-width;
+ max-width: $sicp-code-snippet-max-width;
}
.sicp-workspace-container-container {
@@ -173,11 +218,14 @@ $sicp-background-color: #ffffff;
}
@media only screen and (max-width: 768px) {
+ display: block;
position: fixed;
+ margin: 0;
+ transform: none;
z-index: 20;
top: 0;
left: 0;
- height: calc(100% - 40px);
+ height: calc(100% - 40px); // minus size of control bar
width: 100vw;
}
}
@@ -204,15 +252,6 @@ $sicp-background-color: #ffffff;
}
}
- // text container
- .sicp-text {
- text-align: justify;
- }
-
- .sicp-epigraph {
- text-align: justify;
- }
-
.sicp-attribution {
text-align: right;
}
@@ -220,7 +259,6 @@ $sicp-background-color: #ffffff;
.sicp-exercise {
margin: 10px 0;
padding: 10px;
- text-align: justify;
background-color: $sicp-background-color !important;
.sicp-button-container {
@@ -237,11 +275,24 @@ $sicp-background-color: #ffffff;
}
// Styles for sicp table of contents
-
.sicp-toc {
overflow-y: auto;
text-align: left;
+ .bp3-tree-node-content {
+ height: fit-content;
+ }
+
+ .bp3-tree-node-list {
+ padding: 0;
+ }
+
+ .bp3-tree-node-label {
+ padding-left: 7px;
+ white-space: normal;
+ text-overflow: clip;
+ }
+
.bp3-tree-node-caret {
color: #777777 !important;
scale: 1.25;
@@ -260,13 +311,8 @@ $sicp-background-color: #ffffff;
font-size: larger;
.bp3-tree-node-content {
- height: fit-content;
- padding: 10px 0;
-
- .bp3-tree-node-label {
- white-space: normal;
- text-overflow: clip;
- }
+ padding-top: 10px;
+ padding-bottom: 10px;
}
@media only screen and (max-width: 768px) {
diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss
index 82a65c1552..3eb2a528a9 100755
--- a/src/styles/_workspace.scss
+++ b/src/styles/_workspace.scss
@@ -377,32 +377,6 @@ $code-color-error: #ff4444;
}
}
- ##{$ns}-tab-panel_side-content-tabs_inspector {
- overflow: hidden;
- .inspect-scope-table {
- width: 100%;
- white-space: nowrap;
- table-layout: fixed;
-
- tbody {
- .inspect-table-obj-name {
- width: 20%;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .inspect-table-obj-details {
- display: block;
- code {
- overflow: hidden;
- text-overflow: ellipsis;
- display: block;
- }
- }
- }
- }
- }
-
// Specific CSS for the Stepper tab, since REPL is hidden
##{$ns}-tab-panel_side-content-tabs_subst_visualiser {
height: calc(100% - 60px);
diff --git a/src/styles/_workspaceGreen.scss b/src/styles/_workspaceGreen.scss
index c08028c15e..16d3f5ecf0 100644
--- a/src/styles/_workspaceGreen.scss
+++ b/src/styles/_workspaceGreen.scss
@@ -2,14 +2,25 @@ $pure-green: #00ff00;
$dark-green: #00e000;
.GreenScreen {
// Hide NavigationBar
+ position: absolute;
height: 100vh;
+ width: 100vw;
margin-top: -50px;
- z-index: 1000;
+ z-index: 15;
+ background: $pure-green !important;
.workspace {
background: $pure-green !important;
}
+ #ace-editor {
+ background: $pure-green !important;
+ }
+
+ .side-content-tooltip {
+ background: $pure-green !important;
+ }
+
#brace-editor {
background: $pure-green !important;
}
@@ -18,6 +29,32 @@ $dark-green: #00e000;
background: $dark-green !important;
}
+ /* individual components */
+ .bp3-button {
+ background: $pure-green !important;
+ box-shadow: none !important;
+ }
+
+ .bp3-input {
+ background: $pure-green !important;
+ box-shadow: none !important;
+ }
+
+ .bp3-control-indicator {
+ background: $pure-green !important;
+ border: 0.1rem solid $dark-green !important;
+ }
+
+ .bp3-control-indicator::before {
+ background: $pure-green !important;
+ border: 0.1rem solid $dark-green !important;
+ box-shadow: none !important;
+ }
+
+ .ace_gutter-active-line {
+ background: $pure-green !important;
+ }
+
/* editor specific */
.editor-react-ace {
color: #222222;