From 2870d7661025abcbd984018d7c7223725a377eed Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Fri, 18 Jun 2021 17:02:44 +0800 Subject: [PATCH 01/20] Interactive-SICP: Mobile bug fixes (#1805) * Left align text * Set default scale for images * Increase spacing between toc caret and label * Update failing snapshots --- src/features/sicp/parser/ParseJson.tsx | 2 +- .../__snapshots__/ParseJson.tsx.snap | 2 +- src/styles/_sicp.scss | 23 +++++++------------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/features/sicp/parser/ParseJson.tsx b/src/features/sicp/parser/ParseJson.tsx index 336f5521ca..e52dcc4af2 100644 --- a/src/features/sicp/parser/ParseJson.tsx +++ b/src/features/sicp/parser/ParseJson.tsx @@ -130,7 +130,7 @@ const handleImage = (obj: JsonType, refs: React.MutableRefObject<{}>) => { {obj['id']} ); } else if (obj['snippet']) { diff --git a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap index d4e6b50d93..57fff34e74 100644 --- a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap +++ b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap @@ -141,7 +141,7 @@ exports[`Parse figures FIGURE with image and scale successful 1`] = ` exports[`Parse figures FIGURE with image successful 1`] = ` "
- \\"id\\" + \\"id\\"
name diff --git a/src/styles/_sicp.scss b/src/styles/_sicp.scss index 972e33ce13..bc4ace2f91 100644 --- a/src/styles/_sicp.scss +++ b/src/styles/_sicp.scss @@ -3,7 +3,6 @@ $sicp-background-color: #ffffff; .Sicp { width: 100%; - text-align: justify; color: $sicp-text-color; overflow: auto; background-color: $sicp-background-color; @@ -46,10 +45,6 @@ $sicp-background-color: #ffffff; height: fit-content; background-color: $sicp-background-color; - h1 { - text-align: left; - } - p { display: inline; } @@ -204,15 +199,6 @@ $sicp-background-color: #ffffff; } } - // text container - .sicp-text { - text-align: justify; - } - - .sicp-epigraph { - text-align: justify; - } - .sicp-attribution { text-align: right; } @@ -220,7 +206,6 @@ $sicp-background-color: #ffffff; .sicp-exercise { margin: 10px 0; padding: 10px; - text-align: justify; background-color: $sicp-background-color !important; .sicp-button-container { @@ -242,6 +227,14 @@ $sicp-background-color: #ffffff; overflow-y: auto; text-align: left; + .bp3-tree-node-list { + padding: 0; + } + + .bp3-tree-node-label { + padding-left: 7px; + } + .bp3-tree-node-caret { color: #777777 !important; scale: 1.25; From 35c076d9f18be2f29c80c61d6b057a165975755d Mon Sep 17 00:00:00 2001 From: Cheng Geng <47176493+ChengGeng97@users.noreply.github.com> Date: Fri, 18 Jun 2021 18:09:28 +0800 Subject: [PATCH 02/20] File Explorer fix for #1787 (#1810) * fix: enhanced GitHub buttons in playground * style: yarn run format * fix: close save file dialog after confirming save --- src/commons/gitHubOverlay/FileExplorerDialog.tsx | 1 + 1 file changed, 1 insertion(+) 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( From 840cc1166470920383618919a4ebc15716316672 Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Sat, 19 Jun 2021 14:09:10 +0800 Subject: [PATCH 03/20] SICP-INTERACTIVE: Fix scrolling behaviour (#1811) * Fix scroll into view causing page to shift * Fix prettier formatting --- src/pages/sicp/Sicp.tsx | 8 ++++---- src/styles/_sicp.scss | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index b3abcc199a..74f2bbc9cd 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -109,12 +109,12 @@ const Sicp: React.FC = props => { }, [section]); // Scroll to correct position - React.useLayoutEffect(() => { + React.useEffect(() => { const hash = props.location.hash; if (!hash) { if (topRef.current) { - topRef.current.scrollIntoView(); + topRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } return; } @@ -122,9 +122,9 @@ const Sicp: React.FC = props => { const ref = refs.current[hash]; if (ref) { - ref.scrollIntoView({ block: 'start' }); + ref.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } - }, [props.location.hash, data]); + }, [props.location.hash]); // Close all active code snippet when new page is loaded React.useEffect(() => { diff --git a/src/styles/_sicp.scss b/src/styles/_sicp.scss index bc4ace2f91..1b1557100f 100644 --- a/src/styles/_sicp.scss +++ b/src/styles/_sicp.scss @@ -41,7 +41,6 @@ $sicp-background-color: #ffffff; margin: 1em auto; padding: 0 6em; max-width: 1050px; - height: fit-content; background-color: $sicp-background-color; From a3f10aa3745aac3d1db42dec6af968fd63f992b5 Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Sun, 20 Jun 2021 17:28:26 +0800 Subject: [PATCH 04/20] Fix hulk (#1814) --- src/styles/_workspaceGreen.scss | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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; From 3245d0a3e97550070a43d47717249442808cb6ef Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Mon, 21 Jun 2021 09:07:23 +0800 Subject: [PATCH 05/20] Interactive sicp: add navigation buttons to bottom of page (#1815) * Add buttons to sicp pages * Refactor navbar navigation buttons * Add navigation functionality to buttons * Add tests for table of contents helper * Update snapshots --- .../subcomponents/SicpNavigationBar.tsx | 68 ++++++------------- .../__snapshots__/SicpNavigationBar.tsx.snap | 2 +- src/features/sicp/TableOfContentsHelper.ts | 13 ++++ .../sicp/__tests__/TableOfContentsHelper.ts | 31 +++++++++ src/pages/sicp/Sicp.tsx | 23 ++++++- src/styles/_sicp.scss | 17 +++++ 6 files changed, 103 insertions(+), 51 deletions(-) create mode 100644 src/features/sicp/TableOfContentsHelper.ts create mode 100644 src/features/sicp/__tests__/TableOfContentsHelper.ts diff --git a/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx b/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx index 33046fde69..6dbc46bda3 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('/interactive-sicp/' + 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__/__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/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/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index 74f2bbc9cd..6e2f9384e7 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -1,14 +1,15 @@ import 'katex/dist/katex.min.css'; -import { Classes, NonIdealState, Spinner } from '@blueprintjs/core'; +import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; 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 SicpIndexPage from './subcomponents/SicpIndexPage'; @@ -66,6 +67,7 @@ const Sicp: React.FC = props => { const { section } = useParams<{ section: string }>(); const topRef = React.useRef(null); const refs = React.useRef({}); + const history = useHistory(); // Fetch json data React.useEffect(() => { @@ -137,6 +139,18 @@ const Sicp: React.FC = props => { dispatch(resetWorkspace('sicp')); dispatch(toggleUsingSubst(false, 'sicp')); }; + const handleNavigation = (sect: string | undefined) => { + history.push('/interactive-sicp/' + sect); + }; + + const navigationButtons = ( +
+ {getPrev(section) && ( + + )} + {getNext(section) && } +
+ ); return (
@@ -147,7 +161,10 @@ const Sicp: React.FC = props => { ) : section === 'index' ? ( ) : ( -
{data}
+
+ {data} + {navigationButtons} +
)}
diff --git a/src/styles/_sicp.scss b/src/styles/_sicp.scss index 1b1557100f..93d96ded6d 100644 --- a/src/styles/_sicp.scss +++ b/src/styles/_sicp.scss @@ -44,6 +44,23 @@ $sicp-background-color: #ffffff; height: fit-content; background-color: $sicp-background-color; + .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 { display: inline; } From 434513f31b247a89f417cbe1451600508e41d1e1 Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Mon, 21 Jun 2021 11:43:40 +0800 Subject: [PATCH 06/20] Interactive Sicp Bug Fixes (#1812) * Adjust toc and snippet styles * Adjust styles for non-ideal-state * Add error boundary * Add tests for sicp error component * Change redirect to useHistory hook * Tweak math font size * Remove redundant css * Adjust font size of code block * Move error related files into features folder * Fix scrolling issues * Fix scroll bug * Fix formatting * Separate refs from large sections --- .../sicp/errors/SicpErrorBoundary.tsx | 36 ++++++ src/features/sicp/errors/SicpErrors.tsx | 58 ++++++++++ .../sicp/errors/__tests__/SicpErrors.tsx | 20 ++++ .../__snapshots__/SicpErrors.tsx.snap | 98 +++++++++++++++++ src/features/sicp/parser/ParseJson.tsx | 12 +- .../__snapshots__/ParseJson.tsx.snap | 10 ++ src/pages/sicp/Sicp.tsx | 104 +++++++----------- src/pages/sicp/subcomponents/SicpToc.tsx | 9 +- src/styles/_sicp.scss | 41 ++++--- 9 files changed, 296 insertions(+), 92 deletions(-) create mode 100644 src/features/sicp/errors/SicpErrorBoundary.tsx create mode 100644 src/features/sicp/errors/SicpErrors.tsx create mode 100644 src/features/sicp/errors/__tests__/SicpErrors.tsx create mode 100644 src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap 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 = ( +
+ Something unexpected went wrong trying to load this page. Please try refreshing the page. If the + issue persists, kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + + . +
+); + +const pageNotFoundError = ( +
+ We could not find the page you were looking for. Please check the URL again. If you believe the + URL is correct, kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + + . +
+); + +const parsingError = ( +
+ An error occured while loading the page. Kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + {' '} + and we will get it fixed as soon as possible. +
+); + +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 :( +

+
+
+ We could not find the page you were looking for. Please check the URL again. If you believe the URL is correct, kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + . +
+
+
" +`; + +exports[`Sicp errors: unexpected error renders correctly 1`] = ` +" +
+
+ + + + + error + + + + + +
+ +

+ Something went wrong :( +

+
+
+ Something unexpected went wrong trying to load this page. Please try refreshing the page. If the issue persists, kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + . +
+
+
" +`; + +exports[`Sicp errors: unexpected error renders correctly 2`] = ` +" +
+
+ + + + + error + + + + + +
+ +

+ Something went wrong :( +

+
+
+ An error occured while loading the page. Kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + + and we will get it fixed as soon as possible. +
+
+
" +`; diff --git a/src/features/sicp/parser/ParseJson.tsx b/src/features/sicp/parser/ParseJson.tsx index e52dcc4af2..c46d48752f 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"> +
+
(refs.current[obj['id']!] = ref)} /> {'[' + obj['count'] + '] '} {parseArr(obj['child']!, refs)}
@@ -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'] && (
@@ -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 57fff34e74..1c5ea40bb2 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`] = ` "
+
\\"id\\"
name @@ -141,6 +144,7 @@ exports[`Parse figures FIGURE with image and scale successful 1`] = ` exports[`Parse figures FIGURE with image successful 1`] = ` "
+
\\"id\\"
name @@ -155,6 +159,7 @@ 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 diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index 6e2f9384e7..0be777aa79 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -1,7 +1,6 @@ import 'katex/dist/katex.min.css'; import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; @@ -11,6 +10,8 @@ import { resetWorkspace, toggleUsingSubst } from 'src/commons/workspace/Workspac 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<{}>; @@ -26,49 +27,30 @@ export const CodeSnippetContext = React.createContext({ const loadingComponent = } />; -const unexpectedError = ( -

- Something unexpected went wrong trying to load this page. Please try refreshing the page. If the - issue persists, kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - - . -
-); -const pageNotFoundError = ( -
- We could not find the page you were looking for. Please check the URL again. If you believe the - URL is correct, kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - - . -
-); -const parsingError = ( -
- An error occured while loading the page. Kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - {' '} - and we will get it fixed as soon as possible. -
-); - -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(() => { setLoading(true); @@ -95,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.useEffect(() => { - const hash = props.location.hash; - - if (!hash) { - if (topRef.current) { - topRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } + if (loading) { return; } + const hash = props.location.hash; const ref = refs.current[hash]; - if (ref) { - ref.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - }, [props.location.hash]); + scrollRefIntoView(ref); + }, [props.location.hash, loading]); // Close all active code snippet when new page is loaded React.useEffect(() => { @@ -154,19 +131,22 @@ const Sicp: React.FC = props => { return (
- -
- {loading ? ( -
{loadingComponent}
- ) : section === 'index' ? ( - - ) : ( -
- {data} - {navigationButtons} -
- )} - + + +
+ {loading ? ( +
{loadingComponent}
+ ) : section === 'index' ? ( + + ) : ( +
+ {data} + {navigationButtons} +
+ )} +
+ +
); }; diff --git a/src/pages/sicp/subcomponents/SicpToc.tsx b/src/pages/sicp/subcomponents/SicpToc.tsx index 7664b25da7..ea21f81b07 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('/interactive-sicp/' + String(node.nodeData)); }, - [props] + [history, props] ); return (
- {slug !== '' && } Date: Mon, 21 Jun 2021 20:35:21 +0800 Subject: [PATCH 07/20] P2Fancy: SourceAcademy GitHub Classroom with central repository (#1771) * test: Fixed tests for FileExplorerDialog * test: wrote tests for GitHubMissionDataUtils * test: wrote tests for GitHubMissionBrowserDialog and GitHubSaveDialog * feat: removed unnecessary console log * feat: increased code coverage for GitHubMissionDataUtils * style: run format * refactor: removed unused files * feat: Added back apparently used files * feat: Improved the default text for Task and Mission briefings * feat: added GitHubAssessmentsNavigationBar Shifted working GitHubLogin to the navbar * Revert "feat: added GitHubAssessmentsNavigationBar" This reverts commit f44eb77ca6dd5413088b43110b3dcb52942d214d. * Refactor GitHub missions to use navbar * Yarn run format and test * feat: Added time-based sorting for mission browser * feat: Added due date display * style: fixed imports for GitHubMisisons * refactor: removed outdated code * Updated props for Application and Navbar tests * fixed typo in NavigationBar test * refactor: changed MissionData to use a typescript declaration * refactor: changed MissionMetadata to use a typescript declaration * style: ran yarn format * feat: Made MissionRepoData a typescript type * refactor: changed TaskData to use a typescript declaration * refactor: fixed imports for some files * feat: removed the popover from GitHubLoginButton for better UX * test: Added snapshot test for GitHubAssessmentsNavigationBar * style: ran format * style: used relative imports * removed unused props * Added GitHubMission navlink to mobile * fixed editorvalue in applicationtypes * removed popover for GitHubLoginButton * refactor: used the old buttons for next and previous task * Changed MissionData and MissionMetadata to use TypeScript type declaration. * yarn run format * yarn run format * fixed GitHubmissionDataUtil test * fixed import for GitHubMissionDataUtils * refactor: removed the TaskView button from the control bar * feat: Added error-handling to getContentAsString in GitHubMissionDataUtils * refactor: made the GH Assessments datatypes to be export defaults * refactor: fixed imports * refactor: removed redundant missionTask and missionBriefing types * refactor: renamed _mission.scss for more clarity * test: Fixed test import for GitHubMissionDataUtils * refactor: moved references to store out of FileExplorerDialog * refactor: renamed GitHubMissions to GitHubMissionListing * test: updated tests * refactor: Fixed imports in Application * feat: fixed the case check for mission repos * feat: the mission listing only lists mission repositories that have a METADATA file * feat: Added more user feedback for when loading mission repos * style: ran format * refactor: GitHubAssessment(Workspace) refactored * style: fixed styling in GitHubAssessmentWorkspace * refactor: removed files unused in current phase of project * Added briefing and reset overlay to GitHubAssessmentWorkspace * feat: Added toggle variable to constants for GitHubAssessments * fix: fix for previous commit * fix: display correct task descriptions * feat: Added unsaved changes warning to the save button * fixed GitHubAssessmentWorkspace editCode to use setTaskList * feat: Added re-routing to mission listing if user is not logged in * fix: made correct task descriptions show * feat: changed the filename of the metadata file * style: ran format * feat: Added loading state to GitHubAssessmentWorkspace page * docs: removed unnecessary comments from ControlBarGitHubButtons * docs: removed unnecessary comments * docs: Added comments for ControlBarGitHubButtons * docs: Added comments to GitHubMissionDataUtils * docs: Added documentation for GitHub-related components * style: run yarn format * docs: Added comments to GitHubAssessmentsNavigationBar * docs: Added more documentation to GitHubUtils * docs: Added documentation to GitHubMissionListing * test: updated snapshot for navbar * removed githuboctokitinstance from Application * fixed Playground test props * fix: updated Constants to pass CI * Added: filter for GitHubMissionListing page * let getGitHubOctokitInstance be any type for test * Yarn run format and test * updated GitHubMissionListingFilter Filter is now && for all tags (meaning that mission must contain all tags to be shown. Filter is case insensitive now. * fix: updated .env.example file * fix: force 404 on githubassessments page according to .env variables * fix: removed unused prop, fixed props types in ControlBarGitHubButtons * fix: used explicit prop type in RepositoryDialog and FileExplorerDialog * refactor: slight refactor in FileExplorerDialog * fix: add error pop-up to GitHubMissionDataUtils * fix: fix typo, numProps should be dateProps * fix: moved github mission types into a single file * fix: removed unused props from GitHubAssessmentWorkspace * fix: revert the previous changes that removed props * fix: editCode function no longer mutates original array * fix: removed unnecessary array copying * fix: used properly updated large spinner size * fix: Added explicit typing for GitHubRepositoryInformation retrieved through octokit * fix: removed unused side content type for current phase * fix: Added tracking for unsaved changes after resetting * fix: removed unused Debugger functions from GitHubAssessmentWorksapce * style: yarn run format * fix: removed handleInterruptEval from GitHubAssessmentWorkspace * fix: removed more unused callbacks from GitHubAssessmentWorkspace * fix: renamed SourceChapter * feat: made showOverlay in the same re-render as loading missions * fix: use same onChangeTabs for mobile workspace * Added GitHubCourseListing page * Updated to show mission links * Updated snapshot * Fixed GitHubCourseListing filter * feat: Added explicit typing for Octokit return values * feat: explicitly typed listForAuthenticatedUser * fix: removed unnecessary console log * fix: made GitHubMissionDataUtils octokit param explicitly typed * test: rewrote FileExplorerDialog test * test: rewrote tests for GitHubTreeNodeCreator * refactor: rewrote GitHubMissionListing for better readability * refactor: rewrote GitHubMissionListing to use Promise.all * reactor: rewrote GitHubMissionListing for better clarity * feat: tags are now case-agnostic in GitHubMissionListing * Added feature to filter missions by org and show unaccepted missions * fix: solved compile error due to possible null or undefined * fix: Added handling of case where there are no mission repos * fix: removed currently unused style for missionEditor * fix: Fix style naming for GitHubAssessmentWorkspace * fix: temporarily removed sideContentMissionEditor style * Removed white navbar from mobile GitHubMissionListing * Fixed missing props in NavigationBarMobileSideMenu test * fix: Removed refresh calls in FileExplorerDialog * Replaced deprecated ITreeNode with TreeNodeInfo * fix: Fixed GitHubMissionListing which would sometimes display wrong non-ideal state message * fix: Added login button to GitHubMissionListing in mobile version * test: fixed OctokitTypes to pass CI * test: fixed OctokitTypes to pass CI * test: fixed OctokitTypes to pass CI * test: fixed OctokitTypes to pass CI * fix: reinstated 404 when githubassessments should not be shown * fix: Fixed prop types for ControlBarGitHubButtons * feat: Added onClickReturn callback to next button in GitHubAssessmentWorkspace * fix: Fix issue with save button lighting up * fix: made the save button stop highlighting after save * fix: Made MissionListing asynchronous * fix: Made prompt appear when attempting to navigate away from GitHubAssessmentWorkspace * fix: unexported getTasksData * fix: setCachedTaskList to the original task list * fix: removed OctokitTypes and imported octokit types instead * Updated .env.example file * Add asynchronous fetching of tasks when loading GithubAssessmentWorkspace * updated .env.example * Fixed GitHubMissions Login Logout button visibility * fix: Use store variables for unsaved changes, prevent github logout if there are unsaved changes * test: fix logout github to work for test * fix: added prompt for user when attempting to navigate away from current question with unsaved changes * Fix workspace width issue * feat: Reset to previously saved code when navigating away * GitHub Mission now processes new .CourseInformation.json correctly. * Yarn run format * Added filter for specific named orgs * Updated feature to process course-info.json .metadata file is no longer needed to retrieve mission data from GitHub. orgs without course-info repo will no longer show up. * Update props for GitHubAssessmentsNavigationBar Test * Updated failing snapshots * GitHub Classroom now properly separated into different assessment groups. * fixed typing for store in useSelector * Merged from upstream * Added temporary navbar for mobile GitHubClassroom The page is unusable without at least a navbar * fix: disable teacher mode if student is member of organization * style: yarn run format * Added tests for GitHubClassroom * Merge branch 'P2Fancy' of https://github.com/chekjun/cadet-frontend into P2Fancy * A default course is selected if found * Yarn run format * Refactored GitHubClassroom, Navbar and added AssessmentListing * Updated routing * Remove comments * Update snapshots * Remove outdated css * Added refreshButton, shorter courseNames and sorting by dueDate/published * fix: Prevent saving if assessment is overdue * fix: Removed save dialog for learners in assessment workspace * Fix course selection issue * fix: removed unnecessary variables from MissionMetadata * Added welcome page for no course * Yarn test * Fixed bug with redirecting to GitHub Classrooms * Fix routing location state issue * Fixed bug where non-accepted assessment has "Review Answers" button. * Fix github navbar.. * Update welcome page css Co-authored-by: ChengGeng97 <47176493+ChengGeng97@users.noreply.github.com> Co-authored-by: Martin Henz Co-authored-by: Chow En Rong <53928333+chownces@users.noreply.github.com> --- src/commons/application/Application.tsx | 13 +- .../__snapshots__/Application.tsx.snap | 3 +- .../github/ControlBarGitHubLoginButton.tsx | 3 +- .../GitHubMissionDataUtils.ts | 91 +--- .../GitHubMissionMobileLoginButton.tsx | 3 +- .../githubAssessments/GitHubMissionTypes.ts | 8 - .../__tests__/GitHubMissionDataUtils.ts | 62 +-- src/commons/navigationBar/NavigationBar.tsx | 14 +- .../__snapshots__/NavigationBar.tsx.snap | 14 +- .../GitHubAssessmentsNavigationBar.tsx | 129 ++++-- .../NavigationBarMobileSideMenu.tsx | 4 +- .../GitHubAssessmentsNavigationBar.tsx | 9 +- .../GitHubAssessmentsNavigationBar.tsx.snap | 9 +- .../NavigationBarMobileSideMenu.tsx.snap | 8 +- src/commons/sagas/GitHubPersistenceSaga.ts | 2 - .../SideContentMissionEditor.tsx | 61 +-- .../__tests__/SideContentMissionEditor.tsx | 64 --- src/commons/utils/ParamParseHelper.ts | 2 + .../GitHubAssessmentDefaultValues.ts | 9 +- .../GitHubAssessmentListing.tsx | 162 +++++++ .../GitHubAssessmentWorkspace.tsx | 135 +++--- .../githubAssessments/GitHubClassroom.tsx | 306 ++++++++++++ .../GitHubClassroomWelcome.tsx | 34 ++ .../GitHubMissionListing.tsx | 436 ------------------ .../__tests__/GitHubClassroom.tsx | 126 +++++ .../__snapshots__/GitHubClassroom.tsx.snap | 13 + src/styles/_academy.scss | 1 + src/styles/_commons.scss | 2 +- src/styles/_github.scss | 16 + src/styles/_githubAssessments.scss | 174 ------- 30 files changed, 881 insertions(+), 1032 deletions(-) delete mode 100644 src/commons/sideContent/githubAssessments/__tests__/SideContentMissionEditor.tsx create mode 100644 src/pages/githubAssessments/GitHubAssessmentListing.tsx create mode 100644 src/pages/githubAssessments/GitHubClassroom.tsx create mode 100644 src/pages/githubAssessments/GitHubClassroomWelcome.tsx delete mode 100644 src/pages/githubAssessments/GitHubMissionListing.tsx create mode 100644 src/pages/githubAssessments/__tests__/GitHubClassroom.tsx create mode 100644 src/pages/githubAssessments/__tests__/__snapshots__/GitHubClassroom.tsx.snap diff --git a/src/commons/application/Application.tsx b/src/commons/application/Application.tsx index 5e8e52cf00..a27f747c7a 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,21 +148,15 @@ const Application: React.FC = props => { {Constants.enableGitHubAssessments && ( ( - )} /> )} - {Constants.enableGitHubAssessments && ( - - )} diff --git a/src/commons/application/__tests__/__snapshots__/Application.tsx.snap b/src/commons/application/__tests__/__snapshots__/Application.tsx.snap index 6f5e6060cf..9f088128d6 100644 --- a/src/commons/application/__tests__/__snapshots__/Application.tsx.snap +++ b/src/commons/application/__tests__/__snapshots__/Application.tsx.snap @@ -8,8 +8,7 @@ exports[`Application renders correctly 1`] = ` - - + 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/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/navigationBar/NavigationBar.tsx b/src/commons/navigationBar/NavigationBar.tsx index 72e4de132d..660273d45a 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; @@ -129,7 +128,7 @@ const NavigationBar: React.FC = props => {
GitHub Assessments
@@ -225,10 +224,10 @@ const NavigationBar: React.FC = props => { -
GitHub Assessments
+
Classroom
)} @@ -302,11 +301,6 @@ 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..ad54d31b24 100644 --- a/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -22,10 +22,10 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = ` Playground
- +
- GitHub Assessments + Classroom
@@ -52,9 +52,6 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = ` - - - @@ -85,10 +82,10 @@ exports[`NavigationBar renders correctly with student role 1`] = ` Playground
- +
- GitHub Assessments + Classroom
@@ -125,9 +122,6 @@ 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'} + > +
- +
- GitHub Assessments + Classroom
@@ -77,10 +77,10 @@ exports[`NavigationBarMobileSideMenu renders correctly with student role 1`] = ` Playground
- +
- GitHub Assessments + Classroom
diff --git a/src/commons/sagas/GitHubPersistenceSaga.ts b/src/commons/sagas/GitHubPersistenceSaga.ts index 1d531a76d8..8993dfefe4 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 { @@ -63,7 +62,6 @@ function* githubLogoutSaga() { yield put(actions.removeGitHubOctokitObject()); yield call(showSuccessMessage, `Logged out from GitHub`, 1000); - yield call(history.push, '/githubassessments/missions'); } function* githubOpenFile(): any { 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 (
- - - - - - -
- - - - - = 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/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/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( + () => ( + + ), + [] + ); + + const refreshButton = useMemo( + () => ( + + ), + [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 ( +
+ +
+ Assessment +
+ +
+
+ +

{assessmentOverview.title}

+
{ownerSlashName}
+
+
+ +
+ +
+ +
+ + + {hasDueDate ? 'Due: ' + dueDate : 'No due date'} + +
+ +
+
+
+
+
+ ); +} + +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( - () => ( - - ), - [] - ); - - // 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 = -
-
-
- -
- ); -} - -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/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/_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; From bed351dfabb425a9d6093716d68b1922fe59d3d3 Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Mon, 21 Jun 2021 23:32:18 +0800 Subject: [PATCH 08/20] removing inspector tab (#1816) * removing inspector tab * removing references to 'inspector' * Remove dead code from inspector.js * Remove inspector related styles Co-authored-by: Samuel Fang --- public/externalLibs/inspector/inspector.js | 227 ------------------ .../mobileSideContent/MobileSideContent.tsx | 4 +- src/commons/sagas/WorkspaceSaga.ts | 7 - src/commons/sagas/__tests__/WorkspaceSaga.ts | 1 - src/commons/sideContent/SideContent.tsx | 2 +- .../sideContent/SideContentInspector.tsx | 54 ----- src/commons/sideContent/SideContentTypes.ts | 1 - src/commons/utils/JsSlangHelper.ts | 8 - src/pages/academy/sourcereel/Sourcereel.tsx | 10 - src/pages/playground/Playground.tsx | 12 +- src/pages/sourcecast/Sourcecast.tsx | 10 - src/styles/_workspace.scss | 26 -- 12 files changed, 4 insertions(+), 358 deletions(-) delete mode 100644 src/commons/sideContent/SideContentInspector.tsx 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 += - '
' + - ''; - } - } - 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 = - '
' + 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/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/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/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/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/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/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 86700464d0..0af9c8afc4 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -40,7 +40,6 @@ 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'; @@ -618,8 +617,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 +852,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/sourcecast/Sourcecast.tsx b/src/pages/sourcecast/Sourcecast.tsx index 769e00c584..4c9edca8bc 100644 --- a/src/pages/sourcecast/Sourcecast.tsx +++ b/src/pages/sourcecast/Sourcecast.tsx @@ -20,7 +20,6 @@ import MobileWorkspace, { } from '../../commons/mobileWorkspace/MobileWorkspace'; 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 @@ -259,7 +258,6 @@ const Sourcecast: React.FC = 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/_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); From e7dd7298114e49fab812832986ab250a593df55f Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Tue, 22 Jun 2021 00:03:07 +0800 Subject: [PATCH 09/20] Stepper: improves hotkeys (#1817) * improves hotkeys for stepper; fixes #1751 * improves hotkeys for stepper; fixes #1751 --- .../SideContentSubstVisualizer.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) 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) => () => { From f4c20c589338fd4dd869a64603377bf514687ca3 Mon Sep 17 00:00:00 2001 From: angelsl Date: Tue, 22 Jun 2021 21:57:09 +0800 Subject: [PATCH 10/20] Adjust workflow to deploy to CloudFlare Pages --- .github/workflows/build-development.yml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-development.yml b/.github/workflows/build-development.yml index 482b8b146c..f91a9f3ee4 100644 --- a/.github/workflows/build-development.yml +++ b/.github/workflows/build-development.yml @@ -50,28 +50,13 @@ 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 - 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. - 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 From 761ed2ca47d1c5fa9bd8fa8cd5b183038e7cb032 Mon Sep 17 00:00:00 2001 From: angelsl Date: Tue, 22 Jun 2021 22:37:19 +0800 Subject: [PATCH 11/20] Upload sourcemaps to Sentry --- .github/workflows/build-development.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-development.yml b/.github/workflows/build-development.yml index f91a9f3ee4..00c5e055d6 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: @@ -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)" @@ -53,6 +56,24 @@ jobs: 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 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: | + find -name '*.map' -print -delete - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: From 93e17ac4f04fa65940bcb95e3d1a3a0158af226a Mon Sep 17 00:00:00 2001 From: angelsl Date: Tue, 22 Jun 2021 23:57:34 +0800 Subject: [PATCH 12/20] Edit workflow job name --- .github/workflows/build-development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-development.yml b/.github/workflows/build-development.yml index 00c5e055d6..b51542b7b2 100644 --- a/.github/workflows/build-development.yml +++ b/.github/workflows/build-development.yml @@ -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 From e0efc3af5821274ad702e30e16ee717803b14ae3 Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Wed, 23 Jun 2021 09:03:18 +0800 Subject: [PATCH 13/20] Interactive Sicp: Fix share link not working and increase editor size (#1819) * Fix share links not working in sicp * Increase width of sicp playground * Fix spacing in control bar Co-authored-by: Martin Henz --- .../controlBar/ControlBarShareButton.tsx | 14 +++++- src/commons/utils/Constants.ts | 1 + src/features/sicp/parser/ParseJson.tsx | 2 +- .../__snapshots__/ParseJson.tsx.snap | 6 +-- src/pages/playground/Playground.tsx | 32 ++++++++------ src/pages/sicp/subcomponents/CodeSnippet.tsx | 19 ++++---- src/styles/_sicp.scss | 44 +++++++++++++++++-- 7 files changed, 88 insertions(+), 30 deletions(-) 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/utils/Constants.ts b/src/commons/utils/Constants.ts index 4af409aa82..2c271f6560 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -81,6 +81,7 @@ export enum Links { techSVC = 'mailto:techsvc@comp.nus.edu.sg', techSVCNumber = '6516 2736', textbook = 'https://source-academy.github.io/interactive-sicp/', + playground = 'https://source-academy.github.io/playground', textbookChapter2_2 = 'https://source-academy.github.io/interactive-sicp/2.2', textbookChapter3_2 = 'https://source-academy.github.io/interactive-sicp/3.2', diff --git a/src/features/sicp/parser/ParseJson.tsx b/src/features/sicp/parser/ParseJson.tsx index c46d48752f..b3e7c5cd5a 100644 --- a/src/features/sicp/parser/ParseJson.tsx +++ b/src/features/sicp/parser/ParseJson.tsx @@ -105,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']! }; diff --git a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap index 1c5ea40bb2..ad006b0bca 100644 --- a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap +++ b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap @@ -160,7 +160,7 @@ exports[`Parse figures FIGURE with image successful 1`] = ` exports[`Parse figures FIGURE with snippet successful 1`] = ` "
- +
name @@ -425,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/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 0af9c8afc4..52ebdd2a41 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -44,7 +44,7 @@ import SideContentRemoteExecution from '../../commons/sideContent/SideContentRem 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'; @@ -64,6 +64,7 @@ export type OwnProps = { isSicpEditor?: boolean; initialEditorValueHash?: string; initialPrependHash?: string | undefined; + initialFullProgramHash?: string; handleCloseEditor?: () => void; }; @@ -551,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( () => ({ 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/styles/_sicp.scss b/src/styles/_sicp.scss index 5678f5cefb..9ba2c3f7b4 100644 --- a/src/styles/_sicp.scss +++ b/src/styles/_sicp.scss @@ -1,6 +1,16 @@ $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%; color: $sicp-text-color; @@ -50,8 +60,8 @@ $sicp-background-color: #ffffff; .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; @@ -157,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 { @@ -183,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; } } From 11f2613f84893d82160728fe5decfa573e029e37 Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Wed, 23 Jun 2021 09:19:40 +0800 Subject: [PATCH 14/20] renaming SA playground and GH assessments (#1820) --- src/commons/navigationBar/NavigationBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commons/navigationBar/NavigationBar.tsx b/src/commons/navigationBar/NavigationBar.tsx index 660273d45a..95d048c0d7 100644 --- a/src/commons/navigationBar/NavigationBar.tsx +++ b/src/commons/navigationBar/NavigationBar.tsx @@ -96,7 +96,7 @@ const NavigationBar: React.FC = props => { onClick={() => setMobileSideMenuOpen(false)} > -
GitHub Assessments
+
Classroom
= props => { to="/playground" > -
Source Academy Playground
+
Playground
= props => { to="/githubassessments" > -
GitHub Assessments
+
Classroom
= props => { to="/playground" > -
Source Academy Playground
+
Playground
Date: Wed, 23 Jun 2021 12:22:24 +0800 Subject: [PATCH 15/20] Update links to point to sourceacademy.org --- src/commons/utils/Constants.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts index 2c271f6560..9918614e33 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 interactive-sicp (images and json files) const authProviders: Map = new Map(); @@ -80,11 +80,10 @@ export enum Links { sourceDocs = 'https://source-academy.github.io/source/', techSVC = 'mailto:techsvc@comp.nus.edu.sg', techSVCNumber = '6516 2736', - textbook = 'https://source-academy.github.io/interactive-sicp/', - playground = 'https://source-academy.github.io/playground', - 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/interactive-sicp/', + playground = 'https://sourceacademy.org/playground', + textbookChapter2_2 = 'https://sourceacademy.org/interactive-sicp/2.2', + textbookChapter3_2 = 'https://sourceacademy.org/interactive-sicp/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', From c8676e4fe85994ff9c2ba601e95c836f2dcd26d4 Mon Sep 17 00:00:00 2001 From: angelsl Date: Wed, 23 Jun 2021 23:28:17 +0800 Subject: [PATCH 16/20] Update snapshots --- src/commons/__tests__/__snapshots__/Markdown.tsx.snap | 2 +- .../__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/__tests__/__snapshots__/Markdown.tsx.snap b/src/commons/__tests__/__snapshots__/Markdown.tsx.snap index 7dc4c37d2f..0e73578634 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/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap b/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap index cad1678202..dd40ba535c 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 From 1147d4124b8b4e94d18fb45bd23ba0df0f47b06d Mon Sep 17 00:00:00 2001 From: angelsl Date: Wed, 23 Jun 2021 23:36:29 +0800 Subject: [PATCH 17/20] Update Source documentation links --- .../__tests__/__snapshots__/Markdown.tsx.snap | 2 +- src/commons/utils/Constants.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/commons/__tests__/__snapshots__/Markdown.tsx.snap b/src/commons/__tests__/__snapshots__/Markdown.tsx.snap index 0e73578634..794852d50a 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/utils/Constants.ts b/src/commons/utils/Constants.ts index 9918614e33..7b1f2cfbcc 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -77,7 +77,7 @@ 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://sourceacademy.org/interactive-sicp/', @@ -87,16 +87,16 @@ export enum Links { 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 = { From 8ce30a1c8af6aa49c5db31e3b341ce6f30c4f208 Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Thu, 24 Jun 2021 11:19:02 +0800 Subject: [PATCH 18/20] 2022 Contributors Page (#1821) * 2022 team * prettier * removing irrelevant lines --- .../subcomponents/ContributorsDetails.tsx | 64 +++++++++++++------ src/styles/_contributors.scss | 5 +- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/pages/contributors/subcomponents/ContributorsDetails.tsx b/src/pages/contributors/subcomponents/ContributorsDetails.tsx index b572555f0d..5c3fc01c5d 100644 --- a/src/pages/contributors/subcomponents/ContributorsDetails.tsx +++ b/src/pages/contributors/subcomponents/ContributorsDetails.tsx @@ -11,45 +11,42 @@ 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
- (Game) + (CTO)

{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 +121,8 @@ class ContributorsDetails extends React.Component {

-

- 2020 Leadership +

+ 2020 Leadership (Knight)


@@ -164,6 +161,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/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; + } } } From 12b80abfe7ced861f9857da04ac6d94c0fb94122 Mon Sep 17 00:00:00 2001 From: Samuel Fang Date: Thu, 24 Jun 2021 12:50:04 +0800 Subject: [PATCH 19/20] Update routes from interactive-sicp to sicpjs (#1822) * Update routes from interactive-sicp to sicpjs * Fix linting errors * Update README.md --- README.md | 3 ++- src/commons/__tests__/__snapshots__/Markdown.tsx.snap | 2 +- src/commons/application/Application.tsx | 7 ++++--- .../__tests__/__snapshots__/Application.tsx.snap | 5 +++-- src/commons/navigationBar/NavigationBar.tsx | 10 +++++----- .../__tests__/__snapshots__/NavigationBar.tsx.snap | 8 ++++---- .../subcomponents/NavigationBarMobileSideMenu.tsx | 2 +- .../navigationBar/subcomponents/SicpNavigationBar.tsx | 2 +- .../__snapshots__/NavigationBarMobileSideMenu.tsx.snap | 4 ++-- .../__snapshots__/SideContentEnvVisualizer.tsx.snap | 2 +- src/commons/utils/Constants.ts | 8 ++++---- src/pages/sicp/Sicp.tsx | 2 +- src/pages/sicp/subcomponents/SicpToc.tsx | 2 +- 13 files changed, 30 insertions(+), 27 deletions(-) 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/src/commons/__tests__/__snapshots__/Markdown.tsx.snap b/src/commons/__tests__/__snapshots__/Markdown.tsx.snap index 794852d50a..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 a27f747c7a..f3fdd74e06 100644 --- a/src/commons/application/Application.tsx +++ b/src/commons/application/Application.tsx @@ -158,8 +158,9 @@ const Application: React.FC = props => { /> )} - - + + + {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 9f088128d6..822ba8dbd6 100644 --- a/src/commons/application/__tests__/__snapshots__/Application.tsx.snap +++ b/src/commons/application/__tests__/__snapshots__/Application.tsx.snap @@ -10,8 +10,9 @@ exports[`Application renders correctly 1`] = ` - - + + + diff --git a/src/commons/navigationBar/NavigationBar.tsx b/src/commons/navigationBar/NavigationBar.tsx index 95d048c0d7..edd3afa15e 100644 --- a/src/commons/navigationBar/NavigationBar.tsx +++ b/src/commons/navigationBar/NavigationBar.tsx @@ -106,7 +106,7 @@ const NavigationBar: React.FC = props => { Classes.MINIMAL, Classes.LARGE )} - to="/interactive-sicp/index" + to="/sicpjs/index" onClick={() => setMobileSideMenuOpen(false)} > @@ -136,7 +136,7 @@ const NavigationBar: React.FC = props => {
SICP JS
@@ -156,7 +156,7 @@ const NavigationBar: React.FC = props => {
SICP JS
@@ -234,7 +234,7 @@ const NavigationBar: React.FC = props => {
SICP JS
@@ -301,7 +301,7 @@ const NavigationBar: React.FC = props => { - + diff --git a/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap index ad54d31b24..6196e451eb 100644 --- a/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/commons/navigationBar/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -28,7 +28,7 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = ` Classroom
- +
SICP JS @@ -52,7 +52,7 @@ exports[`NavigationBar renders "Not logged in" correctly 1`] = ` - + @@ -88,7 +88,7 @@ exports[`NavigationBar renders correctly with student role 1`] = ` Classroom
- +
SICP JS @@ -122,7 +122,7 @@ exports[`NavigationBar renders correctly with student role 1`] = ` - + diff --git a/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx b/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx index 89f25a3842..bedd1cd180 100644 --- a/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx +++ b/src/commons/navigationBar/subcomponents/NavigationBarMobileSideMenu.tsx @@ -180,7 +180,7 @@ const NavigationBarMobileSideMenu: React.FC = 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 6dbc46bda3..f5922ef7ea 100644 --- a/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx +++ b/src/commons/navigationBar/subcomponents/SicpNavigationBar.tsx @@ -22,7 +22,7 @@ const SicpNavigationBar: React.FC = () => { const handleCloseToc = () => setIsTocOpen(false); const handleOpenToc = () => setIsTocOpen(true); const handleNavigation = (sect: string) => { - history.push('/interactive-sicp/' + sect); + history.push('/sicpjs/' + sect); }; // Button to open table of contents diff --git a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap index 5c49725e0b..3d7291718f 100644 --- a/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap +++ b/src/commons/navigationBar/subcomponents/__tests__/__snapshots__/NavigationBarMobileSideMenu.tsx.snap @@ -20,7 +20,7 @@ exports[`NavigationBarMobileSideMenu renders "Not logged in" correctly 1`] = ` Classroom
- +
SICP JS @@ -83,7 +83,7 @@ exports[`NavigationBarMobileSideMenu renders correctly with student role 1`] = ` Classroom
- +
SICP JS diff --git a/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap b/src/commons/sideContent/__tests__/__snapshots__/SideContentEnvVisualizer.tsx.snap index dd40ba535c..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/utils/Constants.ts b/src/commons/utils/Constants.ts index 7b1f2cfbcc..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://sicp.sourceacademy.org/'; // 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(); @@ -80,10 +80,10 @@ export enum Links { sourceDocs = 'https://docs.sourceacademy.org/', techSVC = 'mailto:techsvc@comp.nus.edu.sg', techSVCNumber = '6516 2736', - textbook = 'https://sourceacademy.org/interactive-sicp/', + textbook = 'https://sourceacademy.org/sicpjs/', playground = 'https://sourceacademy.org/playground', - textbookChapter2_2 = 'https://sourceacademy.org/interactive-sicp/2.2', - textbookChapter3_2 = 'https://sourceacademy.org/interactive-sicp/3.2', + 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', diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index 0be777aa79..572cf6fee0 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -117,7 +117,7 @@ const Sicp: React.FC = props => { dispatch(toggleUsingSubst(false, 'sicp')); }; const handleNavigation = (sect: string | undefined) => { - history.push('/interactive-sicp/' + sect); + history.push('/sicpjs/' + sect); }; const navigationButtons = ( diff --git a/src/pages/sicp/subcomponents/SicpToc.tsx b/src/pages/sicp/subcomponents/SicpToc.tsx index ea21f81b07..ac1b8c9025 100644 --- a/src/pages/sicp/subcomponents/SicpToc.tsx +++ b/src/pages/sicp/subcomponents/SicpToc.tsx @@ -36,7 +36,7 @@ const SicpToc: React.FC = props => { if (props.handleCloseToc) { props.handleCloseToc(); } - history.push('/interactive-sicp/' + String(node.nodeData)); + history.push('/sicpjs/' + String(node.nodeData)); }, [history, props] ); From ca0dbc4b6755a96e776a98c3208d0e56157d8103 Mon Sep 17 00:00:00 2001 From: Martin Henz Date: Thu, 24 Jun 2021 12:54:02 +0800 Subject: [PATCH 20/20] adding gokul (#1823) --- .../contributors/subcomponents/ContributorsDetails.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/contributors/subcomponents/ContributorsDetails.tsx b/src/pages/contributors/subcomponents/ContributorsDetails.tsx index 5c3fc01c5d..22b935463c 100644 --- a/src/pages/contributors/subcomponents/ContributorsDetails.tsx +++ b/src/pages/contributors/subcomponents/ContributorsDetails.tsx @@ -31,6 +31,12 @@ class ContributorsDetails extends React.Component { (CTO)

{dot} +

+ Gokul Rajiv +
+ (Game) +

+ {dot}

Chow En Rong

' + - k + - '' + - filter(str) + - '
' + filter(frame.name) + '