diff --git a/src/components/academy/grading/index.tsx b/src/components/academy/grading/index.tsx index 2eb9574ff4..60bda31180 100644 --- a/src/components/academy/grading/index.tsx +++ b/src/components/academy/grading/index.tsx @@ -62,7 +62,7 @@ class Grading extends React.Component { if (this.props.gradingOverviews === undefined) { const loadingDisplay = ( } /> @@ -75,7 +75,7 @@ class Grading extends React.Component { ) } const grid = ( -
+
( +const assessmentRenderFactory = (cat: AssessmentCategory) => ( routerProps: RouteComponentProps -) => +) => const assessmentRegExp = ':assessmentId(\\d+)?/:questionId(\\d+)?' @@ -42,24 +42,24 @@ export const Academy: React.SFC = props => ( path={`/academy/${assessmentCategoryLink( AssessmentCategories.Contest )}/${assessmentRegExp}`} - render={assessmentListingRenderFactory(AssessmentCategories.Contest)} + render={assessmentRenderFactory(AssessmentCategories.Contest)} /> diff --git a/src/components/assessment/AssessmentListing.tsx b/src/components/assessment/AssessmentListing.tsx deleted file mode 100644 index 4e8f064ed0..0000000000 --- a/src/components/assessment/AssessmentListing.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Button, Card, Icon, Intent, NonIdealState, Spinner, Text } from '@blueprintjs/core' -import { IconNames } from '@blueprintjs/icons' -import * as React from 'react' -import { RouteComponentProps } from 'react-router' -import { NavLink } from 'react-router-dom' - -import AssessmentContainer from '../../containers/assessment' -import { assessmentCategoryLink, stringParamToInt } from '../../utils/paramParseHelpers' -import { OwnProps as AssessmentProps } from '../assessment' -import { AssessmentCategory } from '../assessment/assessmentShape' -import { IAssessmentOverview } from '../assessment/assessmentShape' -import ContentDisplay, { IContentDisplayProps } from '../commons/ContentDisplay' - -export interface IAssessmentParams { - assessmentId?: string - questionId?: string -} - -export interface IAssessmentListingProps - extends IDispatchProps, - IOwnProps, - RouteComponentProps, - IStateProps {} - -export interface IDispatchProps { - handleAssessmentOverviewFetch: () => void - handleResetAssessmentWorkspace: () => void - handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => void -} - -export interface IOwnProps { - assessmentCategory: AssessmentCategory -} - -export interface IStateProps { - assessmentOverviews?: IAssessmentOverview[] - storedAssessmentId?: number - storedQuestionId?: number -} - -class AssessmentListing extends React.Component { - public componentWillMount() { - const assessmentId = stringParamToInt(this.props.match.params.assessmentId) - const questionId = stringParamToInt(this.props.match.params.questionId) - if (assessmentId === null || questionId === null) { - return - } - - if ( - this.props.storedAssessmentId !== assessmentId || - this.props.storedQuestionId !== questionId - ) { - this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId) - this.props.handleResetAssessmentWorkspace() - } - } - - public render() { - const assessmentId: number | null = stringParamToInt(this.props.match.params.assessmentId) - // default questionId is 0 (the first question) - const questionId: number = stringParamToInt(this.props.match.params.questionId) || 0 - - // if there is no assessmentId specified, Render only information. - if (assessmentId === null) { - const props: IContentDisplayProps = { - display: ( - - ), - loadContentDispatch: this.props.handleAssessmentOverviewFetch - } - return ( -
- -
- ) - } else { - const props: AssessmentProps = { - assessmentId, - questionId - } - return - } - } -} - -interface IAssessmentOverviewCardProps { - assessmentOverviews?: IAssessmentOverview[] - questionId: number -} - -export const AssessmentOverviewCard: React.SFC = props => { - const questionId = props.questionId === undefined ? 0 : props.questionId - if (props.assessmentOverviews === undefined) { - return } /> - } else if (props.assessmentOverviews.length === 0) { - return - } - const cards = props.assessmentOverviews.map((overview, index) => ( -
- -
PICTURE
-
-
-

{overview.title}

-
-
-
Mission 0 : 123123 XP (hardcoded)
-
-
-

{overview.shortSummary}

-
-
-
- - - Due: 12/12/12 - -
-
- - - -
-
-
-
-
- )) - return <>{cards} -} - -export default AssessmentListing diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx new file mode 100644 index 0000000000..bc6e3c0056 --- /dev/null +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -0,0 +1,171 @@ +import { Button, Card, Dialog, NonIdealState, Spinner, Text } from '@blueprintjs/core' +import { IconNames } from '@blueprintjs/icons' +import * as React from 'react' + +import { InterpreterOutput } from '../../reducers/states' +import { history } from '../../utils/history' +import { assessmentCategoryLink } from '../../utils/paramParseHelpers' +import Workspace, { WorkspaceProps } from '../workspace' +import { ControlBarProps } from '../workspace/ControlBar' +import { SideContentProps } from '../workspace/side-content' +import { + IAssessment, + IMCQQuestion, + IProgrammingQuestion, + IQuestion, + QuestionTypes +} from './assessmentShape' + +export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps + +export type StateProps = { + activeTab: number + assessment?: IAssessment + editorValue?: string + editorWidth: string + isRunning: boolean + output: InterpreterOutput[] + replValue: string + sideContentHeight?: number +} + +export type OwnProps = { + assessmentId: number + questionId: number +} + +export type DispatchProps = { + handleAssessmentFetch: (assessmentId: number) => void + handleChangeActiveTab: (activeTab: number) => void + handleChapterSelect: (chapter: any, changeEvent: any) => void + handleEditorEval: () => void + handleEditorValueChange: (val: string) => void + handleEditorWidthChange: (widthChange: number) => void + handleInterruptEval: () => void + handleReplEval: () => void + handleReplOutputClear: () => void + handleReplValueChange: (newValue: string) => void + handleSideContentHeightChange: (heightChange: number) => void +} + +class AssessmentWorkspace extends React.Component< + AssessmentWorkspaceProps, + { showOverlay: boolean } +> { + public state = { showOverlay: false } + + public componentWillMount() { + this.props.handleAssessmentFetch(this.props.assessmentId) + if (this.props.questionId === 0) { + this.setState({ showOverlay: true }) + } + } + + public render() { + if (this.props.assessment === undefined || this.props.assessment.questions.length === 0) { + return ( + } + /> + ) + } + const overlay = ( + + + {this.props.assessment.longSummary} + + ) + const question: IQuestion = this.props.assessment.questions[this.props.questionId] + const workspaceProps: WorkspaceProps = { + controlBarProps: this.controlBarProps(this.props), + editorProps: + question.type === QuestionTypes.programming + ? { + editorValue: + this.props.editorValue !== undefined + ? this.props.editorValue + : (question as IProgrammingQuestion).solutionTemplate, + handleEditorEval: this.props.handleEditorEval, + handleEditorValueChange: this.props.handleEditorValueChange + } + : undefined, + editorWidth: this.props.editorWidth, + handleEditorWidthChange: this.props.handleEditorWidthChange, + handleSideContentHeightChange: this.props.handleSideContentHeightChange, + mcq: question as IMCQQuestion, + sideContentHeight: this.props.sideContentHeight, + sideContentProps: this.sideContentProps(this.props), + replProps: { + output: this.props.output, + replValue: this.props.replValue, + handleReplEval: this.props.handleReplEval, + handleReplValueChange: this.props.handleReplValueChange + } + } + return ( +
+ {overlay} + +
+ ) + } + + /** Pre-condition: IAssessment has been loaded */ + private sideContentProps: (p: AssessmentWorkspaceProps) => SideContentProps = ( + props: AssessmentWorkspaceProps + ) => ({ + activeTab: 0, + handleChangeActiveTab: (aT: number) => {}, + tabs: [ + { + label: `Task ${props.questionId}`, + icon: IconNames.NINJA, + body: {props.assessment!.questions[props.questionId].content} + }, + { + label: `${props.assessment!.category} Briefing`, + icon: IconNames.BRIEFCASE, + body: {props.assessment!.longSummary} + } + ] + }) + + /** Pre-condition: IAssessment has been loaded */ + private controlBarProps: (p: AssessmentWorkspaceProps) => ControlBarProps = ( + props: AssessmentWorkspaceProps + ) => { + const listingPath = `/academy/${assessmentCategoryLink(this.props.assessment!.category)}` + const assessmentWorkspacePath = listingPath + `/${this.props.assessment!.id.toString()}` + return { + handleChapterSelect: this.props.handleChapterSelect, + handleEditorEval: this.props.handleEditorEval, + handleInterruptEval: this.props.handleInterruptEval, + handleReplEval: this.props.handleReplEval, + handleReplOutputClear: this.props.handleReplOutputClear, + hasChapterSelect: false, + hasNextButton: this.props.questionId < this.props.assessment!.questions.length - 1, + hasPreviousButton: this.props.questionId > 0, + hasSaveButton: true, + hasShareButton: false, + hasSubmitButton: this.props.questionId === this.props.assessment!.questions.length - 1, + isRunning: this.props.isRunning, + onClickNext: () => + history.push(assessmentWorkspacePath + `/${(this.props.questionId + 1).toString()}`), + onClickPrevious: () => + history.push(assessmentWorkspacePath + `/${(this.props.questionId - 1).toString()}`), + onClickSubmit: () => history.push(listingPath), + sourceChapter: 2 // TODO dynamic library changing + } + } +} + +export default AssessmentWorkspace diff --git a/src/components/assessment/__tests__/AssessmentListing.tsx b/src/components/assessment/__tests__/AssessmentListing.tsx deleted file mode 100644 index 16aab238fd..0000000000 --- a/src/components/assessment/__tests__/AssessmentListing.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { mount } from 'enzyme' -import * as React from 'react' -import { MemoryRouter } from 'react-router' - -import { mockAssessmentOverviews } from '../../../mocks/assessmentAPI' -import { mockRouterProps } from '../../../mocks/components' -import AssessmentListing, { IAssessmentListingProps } from '../AssessmentListing' -import { AssessmentCategories } from '../assessmentShape' - -const defaultProps: IAssessmentListingProps = { - assessmentCategory: AssessmentCategories.Mission, - assessmentOverviews: undefined, - handleAssessmentOverviewFetch: () => {}, - handleResetAssessmentWorkspace: () => {}, - handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => {}, - ...mockRouterProps('/academy/missions', {}) -} - -const mockUndefinedAssessmentListing: IAssessmentListingProps = { - ...defaultProps, - assessmentOverviews: undefined -} - -const mockEmptyAssessmentListing: IAssessmentListingProps = { - ...defaultProps, - assessmentOverviews: [] -} - -const mockPresentAssessmentListing: IAssessmentListingProps = { - ...defaultProps, - assessmentOverviews: mockAssessmentOverviews -} - -test('AssessmentListing page "loading" content renders correctly', () => { - const app = ( - - - - ) - const tree = mount(app) - expect(tree.debug()).toMatchSnapshot() -}) - -test('AssessmentListing page with 0 missions renders correctly', () => { - const app = ( - - - - ) - const tree = mount(app) - expect(tree.debug()).toMatchSnapshot() -}) - -test('AssessmentListing page with multiple loaded missions renders correctly', () => { - const app = ( - - - - ) - const tree = mount(app) - expect(tree.debug()).toMatchSnapshot() -}) diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx new file mode 100644 index 0000000000..2522d2b041 --- /dev/null +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -0,0 +1,62 @@ +import { shallow } from 'enzyme' +import * as React from 'react' + +import { mockAssessments } from '../../../mocks/assessmentAPI' +import AssessmentWorkspace, { AssessmentWorkspaceProps } from '../AssessmentWorkspace' + +const defaultProps: AssessmentWorkspaceProps = { + activeTab: 0, + assessmentId: 0, + editorWidth: '50%', + handleAssessmentFetch: (assessmentId: number) => {}, + handleChangeActiveTab: (activeTab: number) => {}, + handleChapterSelect: (chapter: any, changeEvent: any) => {}, + handleEditorEval: () => {}, + handleEditorValueChange: (val: string) => {}, + handleEditorWidthChange: (widthChange: number) => {}, + handleInterruptEval: () => {}, + handleReplEval: () => {}, + handleReplOutputClear: () => {}, + handleReplValueChange: (newValue: string) => {}, + handleSideContentHeightChange: (heightChange: number) => {}, + isRunning: false, + output: [], + questionId: 0, + replValue: '' +} + +const mockUndefinedAssessmentWorkspaceProps: AssessmentWorkspaceProps = { + ...defaultProps +} + +const mockProgrammingAssessmentWorkspaceProps: AssessmentWorkspaceProps = { + ...defaultProps, + assessment: mockAssessments[0], + assessmentId: 0, + questionId: 0 +} + +const mockMcqAssessmentWorkspaceProps: AssessmentWorkspaceProps = { + ...defaultProps, + assessment: mockAssessments[0], + assessmentId: 0, + questionId: 2 +} + +test('AssessmentWorkspace page "loading" content renders correctly', () => { + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) + +test('AssessmentWorkspace page with programming question renders correctly', () => { + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) + +test('AssessmentWorkspace page with MCQ question renders correctly', () => { + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) diff --git a/src/components/assessment/__tests__/__snapshots__/AssessmentListing.tsx.snap b/src/components/assessment/__tests__/__snapshots__/AssessmentListing.tsx.snap deleted file mode 100644 index c6859ed306..0000000000 --- a/src/components/assessment/__tests__/__snapshots__/AssessmentListing.tsx.snap +++ /dev/null @@ -1,451 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AssessmentListing page "loading" content renders correctly 1`] = ` -" - - -
- -
-
- -
- - -
-
- -
-
- - - - -
-
-
-
-
- Fetching assessment... -
-
-
-
-
-
-
-
-
-
-
-
-
" -`; - -exports[`AssessmentListing page with 0 missions renders correctly 1`] = ` -" - - -
- -
-
- -
- - -
-
- - - - flame - - - - -
-

- There are no assessments. -

-
-
-
-
-
-
-
-
-
-
-
-
" -`; - -exports[`AssessmentListing page with multiple loaded missions renders correctly 1`] = ` -" - - -
- -
-
- -
- -
- -
-
- PICTURE -
-
-
-

- An Odessey to Runes -

-
-
-
- Mission 0 : 123123 XP (hardcoded) -
-
-
-

- Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. -

-
-
-
- -
- - - - time - - - - - Due: 12/12/12 -
-
-
- -
-
-
-
-
-
- -
-
- PICTURE -
-
-
-

- The Secret to Streams -

-
-
-
- Mission 0 : 123123 XP (hardcoded) -
-
-
-

- Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. -

-
-
-
- -
- - - - time - - - - - Due: 12/12/12 -
-
-
- -
-
-
-
-
-
- -
-
- PICTURE -
-
-
-

- A sample Sidequest -

-
-
-
- Mission 0 : 123123 XP (hardcoded) -
-
-
-

- Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. -

-
-
-
- -
- - - - time - - - - - Due: 12/12/12 -
-
-
- -
-
-
-
-
-
- -
-
- PICTURE -
-
-
-

- A closed Mission -

-
-
-
- Mission 0 : 123123 XP (hardcoded) -
-
-
-

- Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. -

-
-
-
- -
- - - - time - - - - - Due: 12/12/12 -
-
-
- -
-
-
-
-
-
- -
-
- PICTURE -
-
-
-

- A closed sidequest -

-
-
-
- Mission 0 : 123123 XP (hardcoded) -
-
-
-

- Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. -

-
-
-
- -
- - - - time - - - - - Due: 12/12/12 -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
" -`; diff --git a/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap b/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap new file mode 100644 index 0000000000..e5c70b566e --- /dev/null +++ b/src/components/assessment/__tests__/__snapshots__/AssessmentWorkspace.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssessmentWorkspace page "loading" content renders correctly 1`] = `""`; + +exports[`AssessmentWorkspace page with MCQ question renders correctly 1`] = ` +"
+ + + + + This is the mission briefing. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra, sem scelerisque ultricies ullamcorper, sem nibh sollicitudin enim, at ultricies sem orci eget odio. Pellentesque varius et mauris quis vestibulum. Etiam in egestas dolor. Nunc consectetur, sapien sodales accumsan convallis, lectus mi tempus ipsum, vel ornare metus turpis sed justo. Vivamus at tellus sed ex convallis commodo at in lectus. Pellentesque pharetra pulvinar sapien pellentesque facilisis. Curabitur efficitur malesuada urna sed aliquam. Quisque massa metus, aliquam in sagittis non, cursus in sem. Morbi vel nunc at nunc pharetra lobortis. Aliquam feugiat ultricies ipsum vel sollicitudin. Vivamus nulla massa, hendrerit sit amet nibh quis, porttitor convallis nisi. + + + + + + +
" +`; + +exports[`AssessmentWorkspace page with programming question renders correctly 1`] = ` +"
+ + + + + This is the mission briefing. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra, sem scelerisque ultricies ullamcorper, sem nibh sollicitudin enim, at ultricies sem orci eget odio. Pellentesque varius et mauris quis vestibulum. Etiam in egestas dolor. Nunc consectetur, sapien sodales accumsan convallis, lectus mi tempus ipsum, vel ornare metus turpis sed justo. Vivamus at tellus sed ex convallis commodo at in lectus. Pellentesque pharetra pulvinar sapien pellentesque facilisis. Curabitur efficitur malesuada urna sed aliquam. Quisque massa metus, aliquam in sagittis non, cursus in sem. Morbi vel nunc at nunc pharetra lobortis. Aliquam feugiat ultricies ipsum vel sollicitudin. Vivamus nulla massa, hendrerit sit amet nibh quis, porttitor convallis nisi. + + + + + + +
" +`; diff --git a/src/components/assessment/__tests__/__snapshots__/index.tsx.snap b/src/components/assessment/__tests__/__snapshots__/index.tsx.snap index fed4a8f1d4..0caa1fc3da 100644 --- a/src/components/assessment/__tests__/__snapshots__/index.tsx.snap +++ b/src/components/assessment/__tests__/__snapshots__/index.tsx.snap @@ -1,35 +1,451 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Assessment page "loading" content renders correctly 1`] = `""`; +exports[`Assessment page "loading" content renders correctly 1`] = ` +" + + +
+ +
+
+ +
+ + +
+
+ +
+
+ + + + +
+
+
+
+
+ Fetching assessment... +
+
+
+
+
+
+
+
+
+
+
+
+
" +`; -exports[`Assessment page with MCQ question renders correctly 1`] = ` -"
- - - - - This is the mission briefing. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra, sem scelerisque ultricies ullamcorper, sem nibh sollicitudin enim, at ultricies sem orci eget odio. Pellentesque varius et mauris quis vestibulum. Etiam in egestas dolor. Nunc consectetur, sapien sodales accumsan convallis, lectus mi tempus ipsum, vel ornare metus turpis sed justo. Vivamus at tellus sed ex convallis commodo at in lectus. Pellentesque pharetra pulvinar sapien pellentesque facilisis. Curabitur efficitur malesuada urna sed aliquam. Quisque massa metus, aliquam in sagittis non, cursus in sem. Morbi vel nunc at nunc pharetra lobortis. Aliquam feugiat ultricies ipsum vel sollicitudin. Vivamus nulla massa, hendrerit sit amet nibh quis, porttitor convallis nisi. - - - - - - -
" +exports[`Assessment page with 0 missions renders correctly 1`] = ` +" + + +
+ +
+
+ +
+ + +
+
+ + + + flame + + + + +
+

+ There are no assessments. +

+
+
+
+
+
+
+
+
+
+
+
+
" `; -exports[`Assessment page with programming question renders correctly 1`] = ` -"
- - - - - This is the mission briefing. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas viverra, sem scelerisque ultricies ullamcorper, sem nibh sollicitudin enim, at ultricies sem orci eget odio. Pellentesque varius et mauris quis vestibulum. Etiam in egestas dolor. Nunc consectetur, sapien sodales accumsan convallis, lectus mi tempus ipsum, vel ornare metus turpis sed justo. Vivamus at tellus sed ex convallis commodo at in lectus. Pellentesque pharetra pulvinar sapien pellentesque facilisis. Curabitur efficitur malesuada urna sed aliquam. Quisque massa metus, aliquam in sagittis non, cursus in sem. Morbi vel nunc at nunc pharetra lobortis. Aliquam feugiat ultricies ipsum vel sollicitudin. Vivamus nulla massa, hendrerit sit amet nibh quis, porttitor convallis nisi. - - - - - - -
" +exports[`Assessment page with multiple loaded missions renders correctly 1`] = ` +" + + +
+ +
+
+ +
+ +
+ +
+
+ PICTURE +
+
+
+

+ An Odessey to Runes +

+
+
+
+ Mission 0 : 123123 XP (hardcoded) +
+
+
+

+ Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. +

+
+
+
+ +
+ + + + time + + + + + Due: 12/12/12 +
+
+
+ +
+
+
+
+
+
+ +
+
+ PICTURE +
+
+
+

+ The Secret to Streams +

+
+
+
+ Mission 0 : 123123 XP (hardcoded) +
+
+
+

+ Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. +

+
+
+
+ +
+ + + + time + + + + + Due: 12/12/12 +
+
+
+ +
+
+
+
+
+
+ +
+
+ PICTURE +
+
+
+

+ A sample Sidequest +

+
+
+
+ Mission 0 : 123123 XP (hardcoded) +
+
+
+

+ Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. +

+
+
+
+ +
+ + + + time + + + + + Due: 12/12/12 +
+
+
+ +
+
+
+
+
+
+ +
+
+ PICTURE +
+
+
+

+ A closed Mission +

+
+
+
+ Mission 0 : 123123 XP (hardcoded) +
+
+
+

+ Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. +

+
+
+
+ +
+ + + + time + + + + + Due: 12/12/12 +
+
+
+ +
+
+
+
+
+
+ +
+
+ PICTURE +
+
+
+

+ A closed sidequest +

+
+
+
+ Mission 0 : 123123 XP (hardcoded) +
+
+
+

+ Once upon a time, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nec vulputate sapien. Fusce vel lacus fermentum, efficitur ipsum. +

+
+
+
+ +
+ + + + time + + + + + Due: 12/12/12 +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
" `; diff --git a/src/components/assessment/__tests__/index.tsx b/src/components/assessment/__tests__/index.tsx index 765136cc27..e448a0796f 100644 --- a/src/components/assessment/__tests__/index.tsx +++ b/src/components/assessment/__tests__/index.tsx @@ -1,62 +1,62 @@ -import { shallow } from 'enzyme' +import { mount } from 'enzyme' import * as React from 'react' - -import { mockAssessments } from '../../../mocks/assessmentAPI' -import Assessment, { AssessmentProps } from '../index' - -const defaultProps: AssessmentProps = { - activeTab: 0, - assessmentId: 0, - editorWidth: '50%', - handleAssessmentFetch: (assessmentId: number) => {}, - handleChangeActiveTab: (activeTab: number) => {}, - handleChapterSelect: (chapter: any, changeEvent: any) => {}, - handleEditorEval: () => {}, - handleEditorValueChange: (val: string) => {}, - handleEditorWidthChange: (widthChange: number) => {}, - handleInterruptEval: () => {}, - handleReplEval: () => {}, - handleReplOutputClear: () => {}, - handleReplValueChange: (newValue: string) => {}, - handleSideContentHeightChange: (heightChange: number) => {}, - isRunning: false, - output: [], - questionId: 0, - replValue: '' +import { MemoryRouter } from 'react-router' + +import Assessment, { IAssessmentProps } from '../' +import { mockAssessmentOverviews } from '../../../mocks/assessmentAPI' +import { mockRouterProps } from '../../../mocks/components' +import { AssessmentCategories } from '../assessmentShape' + +const defaultProps: IAssessmentProps = { + assessmentCategory: AssessmentCategories.Mission, + assessmentOverviews: undefined, + handleAssessmentOverviewFetch: () => {}, + handleResetAssessmentWorkspace: () => {}, + handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => {}, + ...mockRouterProps('/academy/missions', {}) } -const mockUndefinedAssessmentProps: AssessmentProps = { - ...defaultProps +const mockUndefinedAssessment: IAssessmentProps = { + ...defaultProps, + assessmentOverviews: undefined } -const mockProgrammingAssessmentProps: AssessmentProps = { +const mockEmptyAssessment: IAssessmentProps = { ...defaultProps, - assessment: mockAssessments[0], - assessmentId: 0, - questionId: 0 + assessmentOverviews: [] } -const mockMcqAssessmentProps: AssessmentProps = { +const mockPresentAssessment: IAssessmentProps = { ...defaultProps, - assessment: mockAssessments[0], - assessmentId: 0, - questionId: 2 + assessmentOverviews: mockAssessmentOverviews } test('Assessment page "loading" content renders correctly', () => { - const app = - const tree = shallow(app) + const app = ( + + + + ) + const tree = mount(app) expect(tree.debug()).toMatchSnapshot() }) -test('Assessment page with programming question renders correctly', () => { - const app = - const tree = shallow(app) +test('Assessment page with 0 missions renders correctly', () => { + const app = ( + + + + ) + const tree = mount(app) expect(tree.debug()).toMatchSnapshot() }) -test('Assessment page with MCQ question renders correctly', () => { - const app = - const tree = shallow(app) +test('Assessment page with multiple loaded missions renders correctly', () => { + const app = ( + + + + ) + const tree = mount(app) expect(tree.debug()).toMatchSnapshot() }) diff --git a/src/components/assessment/index.tsx b/src/components/assessment/index.tsx index a897b7f663..53123ed915 100644 --- a/src/components/assessment/index.tsx +++ b/src/components/assessment/index.tsx @@ -1,166 +1,145 @@ -import { Button, Card, Dialog, NonIdealState, Spinner, Text } from '@blueprintjs/core' +import { Button, Card, Icon, Intent, NonIdealState, Spinner, Text } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons' import * as React from 'react' +import { RouteComponentProps } from 'react-router' +import { NavLink } from 'react-router-dom' -import { InterpreterOutput } from '../../reducers/states' -import { history } from '../../utils/history' -import { assessmentCategoryLink } from '../../utils/paramParseHelpers' -import Workspace, { WorkspaceProps } from '../workspace' -import { ControlBarProps } from '../workspace/ControlBar' -import { SideContentProps } from '../workspace/side-content' -import { - IAssessment, - IMCQQuestion, - IProgrammingQuestion, - IQuestion, - QuestionTypes -} from './assessmentShape' +import AssessmentWorkspaceContainer from '../../containers/assessment/AssessmentWorkspaceContainer' +import { assessmentCategoryLink, stringParamToInt } from '../../utils/paramParseHelpers' +import { AssessmentCategory, IAssessmentOverview } from '../assessment/assessmentShape' +import { OwnProps as AssessmentProps } from '../assessment/AssessmentWorkspace' +import ContentDisplay, { IContentDisplayProps } from '../commons/ContentDisplay' -export type AssessmentProps = DispatchProps & OwnProps & StateProps - -export type StateProps = { - activeTab: number - assessment?: IAssessment - editorValue?: string - editorWidth: string - isRunning: boolean - output: InterpreterOutput[] - replValue: string - sideContentHeight?: number +export interface IAssessmentWorkspaceParams { + assessmentId?: string + questionId?: string } -export type OwnProps = { - assessmentId: number - questionId: number +export interface IAssessmentProps + extends IDispatchProps, + IOwnProps, + RouteComponentProps, + IStateProps {} + +export interface IDispatchProps { + handleAssessmentOverviewFetch: () => void + handleResetAssessmentWorkspace: () => void + handleUpdateCurrentAssessmentId: (assessmentId: number, questionId: number) => void } -export type DispatchProps = { - handleAssessmentFetch: (assessmentId: number) => void - handleChangeActiveTab: (activeTab: number) => void - handleChapterSelect: (chapter: any, changeEvent: any) => void - handleEditorEval: () => void - handleEditorValueChange: (val: string) => void - handleEditorWidthChange: (widthChange: number) => void - handleInterruptEval: () => void - handleReplEval: () => void - handleReplOutputClear: () => void - handleReplValueChange: (newValue: string) => void - handleSideContentHeightChange: (heightChange: number) => void +export interface IOwnProps { + assessmentCategory: AssessmentCategory } -class Assessment extends React.Component { - public state = { showOverlay: false } +export interface IStateProps { + assessmentOverviews?: IAssessmentOverview[] + storedAssessmentId?: number + storedQuestionId?: number +} +class Assessment extends React.Component { public componentWillMount() { - this.props.handleAssessmentFetch(this.props.assessmentId) - if (this.props.questionId === 0) { - this.setState({ showOverlay: true }) + const assessmentId = stringParamToInt(this.props.match.params.assessmentId) + const questionId = stringParamToInt(this.props.match.params.questionId) + if (assessmentId === null || questionId === null) { + return + } + + if ( + this.props.storedAssessmentId !== assessmentId || + this.props.storedQuestionId !== questionId + ) { + this.props.handleUpdateCurrentAssessmentId(assessmentId, questionId) + this.props.handleResetAssessmentWorkspace() } } public render() { - if (this.props.assessment === undefined || this.props.assessment.questions.length === 0) { + const assessmentId: number | null = stringParamToInt(this.props.match.params.assessmentId) + // default questionId is 0 (the first question) + const questionId: number = stringParamToInt(this.props.match.params.questionId) || 0 + + // if there is no assessmentId specified, Render only information. + if (assessmentId === null) { + const props: IContentDisplayProps = { + display: ( + + ), + loadContentDispatch: this.props.handleAssessmentOverviewFetch + } return ( - } - /> +
+ +
) - } - const overlay = ( - - - {this.props.assessment.longSummary} - - ) - const question: IQuestion = this.props.assessment.questions[this.props.questionId] - const workspaceProps: WorkspaceProps = { - controlBarProps: this.controlBarProps(this.props), - editorProps: - question.type === QuestionTypes.programming - ? { - editorValue: - this.props.editorValue !== undefined - ? this.props.editorValue - : (question as IProgrammingQuestion).solutionTemplate, - handleEditorEval: this.props.handleEditorEval, - handleEditorValueChange: this.props.handleEditorValueChange - } - : undefined, - editorWidth: this.props.editorWidth, - handleEditorWidthChange: this.props.handleEditorWidthChange, - handleSideContentHeightChange: this.props.handleSideContentHeightChange, - mcq: question as IMCQQuestion, - sideContentHeight: this.props.sideContentHeight, - sideContentProps: this.sideContentProps(this.props), - replProps: { - output: this.props.output, - replValue: this.props.replValue, - handleReplEval: this.props.handleReplEval, - handleReplValueChange: this.props.handleReplValueChange + } else { + const props: AssessmentProps = { + assessmentId, + questionId } + return } - return ( -
- {overlay} - -
- ) } +} - /** Pre-condition: IAssessment has been loaded */ - private sideContentProps: (p: AssessmentProps) => SideContentProps = ( - props: AssessmentProps - ) => ({ - activeTab: 0, - handleChangeActiveTab: (aT: number) => {}, - tabs: [ - { - label: `Task ${props.questionId}`, - icon: IconNames.NINJA, - body: {props.assessment!.questions[props.questionId].content} - }, - { - label: `${props.assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, - body: {props.assessment!.longSummary} - } - ] - }) +interface IAssessmentOverviewCardProps { + assessmentOverviews?: IAssessmentOverview[] + questionId: number +} - /** Pre-condition: IAssessment has been loaded */ - private controlBarProps: (p: AssessmentProps) => ControlBarProps = (props: AssessmentProps) => { - const listingPath = `/academy/${assessmentCategoryLink(this.props.assessment!.category)}` - const assessmentPath = listingPath + `/${this.props.assessment!.id.toString()}` - return { - handleChapterSelect: this.props.handleChapterSelect, - handleEditorEval: this.props.handleEditorEval, - handleInterruptEval: this.props.handleInterruptEval, - handleReplEval: this.props.handleReplEval, - handleReplOutputClear: this.props.handleReplOutputClear, - hasChapterSelect: false, - hasNextButton: this.props.questionId < this.props.assessment!.questions.length - 1, - hasPreviousButton: this.props.questionId > 0, - hasSaveButton: true, - hasShareButton: false, - hasSubmitButton: this.props.questionId === this.props.assessment!.questions.length - 1, - isRunning: this.props.isRunning, - onClickNext: () => - history.push(assessmentPath + `/${(this.props.questionId + 1).toString()}`), - onClickPrevious: () => - history.push(assessmentPath + `/${(this.props.questionId - 1).toString()}`), - onClickSubmit: () => history.push(listingPath), - sourceChapter: 2 // TODO dynamic library changing - } +export const AssessmentOverviewCard: React.SFC = props => { + const questionId = props.questionId === undefined ? 0 : props.questionId + if (props.assessmentOverviews === undefined) { + return } /> + } else if (props.assessmentOverviews.length === 0) { + return } + const cards = props.assessmentOverviews.map((overview, index) => ( +
+ +
PICTURE
+
+
+

{overview.title}

+
+
+
Mission 0 : 123123 XP (hardcoded)
+
+
+

{overview.shortSummary}

+
+
+
+ + + Due: 12/12/12 + +
+
+ + + +
+
+
+
+
+ )) + return <>{cards} } export default Assessment diff --git a/src/containers/assessment/AssessmentListingContainer.ts b/src/containers/assessment/AssessmentListingContainer.ts deleted file mode 100644 index 7ead6b4bc4..0000000000 --- a/src/containers/assessment/AssessmentListingContainer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' -import { withRouter } from 'react-router' -import { bindActionCreators, Dispatch } from 'redux' - -import { fetchAssessmentOverviews } from '../../actions/session' -import { resetAssessmentWorkspace, updateCurrentAssessmentId } from '../../actions/workspaces' -import AssessmentListing, { - IDispatchProps, - IOwnProps, - IStateProps -} from '../../components/assessment/AssessmentListing' -import { IAssessmentOverview } from '../../components/assessment/assessmentShape' -import { IState } from '../../reducers/states' - -const mapStateToProps: MapStateToProps = (state, props) => { - const categoryFilter = (overview: IAssessmentOverview) => - overview.category === props.assessmentCategory - const stateProps: IStateProps = { - assessmentOverviews: state.session.assessmentOverviews - ? state.session.assessmentOverviews.filter(categoryFilter) - : undefined, - storedAssessmentId: state.workspaces.currentAssessment, - storedQuestionId: state.workspaces.currentQuestion - } - return stateProps -} - -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( - { - handleAssessmentOverviewFetch: fetchAssessmentOverviews, - handleResetAssessmentWorkspace: resetAssessmentWorkspace, - handleUpdateCurrentAssessmentId: updateCurrentAssessmentId - }, - dispatch - ) - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AssessmentListing)) diff --git a/src/containers/assessment/AssessmentWorkspaceContainer.ts b/src/containers/assessment/AssessmentWorkspaceContainer.ts new file mode 100644 index 0000000000..48bcff99e1 --- /dev/null +++ b/src/containers/assessment/AssessmentWorkspaceContainer.ts @@ -0,0 +1,60 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' +import { bindActionCreators, Dispatch } from 'redux' + +import { + changeActiveTab, + changeEditorWidth, + changeSideContentHeight, + chapterSelect, + clearReplOutput, + evalEditor, + evalRepl, + fetchAssessment, + handleInterruptExecution, + updateEditorValue, + updateReplValue, + WorkspaceLocation +} from '../../actions' +import AssessmentWorkspace, { + DispatchProps, + OwnProps, + StateProps +} from '../../components/assessment/AssessmentWorkspace' +import { IState } from '../../reducers/states' + +const mapStateToProps: MapStateToProps = (state, props) => { + return { + assessment: state.session.assessments.get(props.assessmentId), + editorValue: state.workspaces.assessment.editorValue, + isRunning: state.workspaces.assessment.isRunning, + activeTab: state.workspaces.assessment.sideContentActiveTab, + editorWidth: state.workspaces.assessment.editorWidth, + sideContentHeight: state.workspaces.assessment.sideContentHeight, + output: state.workspaces.assessment.output, + replValue: state.workspaces.assessment.replValue + } +} + +const location: WorkspaceLocation = 'assessment' + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleAssessmentFetch: fetchAssessment, + handleChangeActiveTab: (activeTab: number) => changeActiveTab(activeTab, location), + handleChapterSelect: (chapter: any, changeEvent: any) => + chapterSelect(chapter, changeEvent, location), + handleEditorEval: () => evalEditor(location), + handleEditorValueChange: (val: string) => updateEditorValue(val, location), + handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, location), + handleInterruptEval: () => handleInterruptExecution(location), + handleReplEval: () => evalRepl(location), + handleReplOutputClear: () => clearReplOutput(location), + handleReplValueChange: (newValue: string) => updateReplValue(newValue, location), + handleSideContentHeightChange: (heightChange: number) => + changeSideContentHeight(heightChange, location) + }, + dispatch + ) + +export default connect(mapStateToProps, mapDispatchToProps)(AssessmentWorkspace) diff --git a/src/containers/assessment/index.ts b/src/containers/assessment/index.ts index 2776b84141..0af85e97c6 100644 --- a/src/containers/assessment/index.ts +++ b/src/containers/assessment/index.ts @@ -1,56 +1,34 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' +import { withRouter } from 'react-router' import { bindActionCreators, Dispatch } from 'redux' -import { - changeActiveTab, - changeEditorWidth, - changeSideContentHeight, - chapterSelect, - clearReplOutput, - evalEditor, - evalRepl, - fetchAssessment, - handleInterruptExecution, - updateEditorValue, - updateReplValue, - WorkspaceLocation -} from '../../actions' -import Assessment, { DispatchProps, OwnProps, StateProps } from '../../components/assessment' +import { fetchAssessmentOverviews } from '../../actions/session' +import { resetAssessmentWorkspace, updateCurrentAssessmentId } from '../../actions/workspaces' +import Assessment, { IDispatchProps, IOwnProps, IStateProps } from '../../components/assessment' +import { IAssessmentOverview } from '../../components/assessment/assessmentShape' import { IState } from '../../reducers/states' -const mapStateToProps: MapStateToProps = (state, props) => { - return { - assessment: state.session.assessments.get(props.assessmentId), - editorValue: state.workspaces.assessment.editorValue, - isRunning: state.workspaces.assessment.isRunning, - activeTab: state.workspaces.assessment.sideContentActiveTab, - editorWidth: state.workspaces.assessment.editorWidth, - sideContentHeight: state.workspaces.assessment.sideContentHeight, - output: state.workspaces.assessment.output, - replValue: state.workspaces.assessment.replValue +const mapStateToProps: MapStateToProps = (state, props) => { + const categoryFilter = (overview: IAssessmentOverview) => + overview.category === props.assessmentCategory + const stateProps: IStateProps = { + assessmentOverviews: state.session.assessmentOverviews + ? state.session.assessmentOverviews.filter(categoryFilter) + : undefined, + storedAssessmentId: state.workspaces.currentAssessment, + storedQuestionId: state.workspaces.currentQuestion } + return stateProps } -const location: WorkspaceLocation = 'assessment' - -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( { - handleAssessmentFetch: fetchAssessment, - handleChangeActiveTab: (activeTab: number) => changeActiveTab(activeTab, location), - handleChapterSelect: (chapter: any, changeEvent: any) => - chapterSelect(chapter, changeEvent, location), - handleEditorEval: () => evalEditor(location), - handleEditorValueChange: (val: string) => updateEditorValue(val, location), - handleEditorWidthChange: (widthChange: number) => changeEditorWidth(widthChange, location), - handleInterruptEval: () => handleInterruptExecution(location), - handleReplEval: () => evalRepl(location), - handleReplOutputClear: () => clearReplOutput(location), - handleReplValueChange: (newValue: string) => updateReplValue(newValue, location), - handleSideContentHeightChange: (heightChange: number) => - changeSideContentHeight(heightChange, location) + handleAssessmentOverviewFetch: fetchAssessmentOverviews, + handleResetAssessmentWorkspace: resetAssessmentWorkspace, + handleUpdateCurrentAssessmentId: updateCurrentAssessmentId }, dispatch ) -export default connect(mapStateToProps, mapDispatchToProps)(Assessment) +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Assessment)) diff --git a/src/styles/_academy.scss b/src/styles/_academy.scss index 350c863ce6..607b162c6b 100644 --- a/src/styles/_academy.scss +++ b/src/styles/_academy.scss @@ -18,7 +18,7 @@ } } -.GradingListing { +.Grading { /* To show the table borders that get cut out by a Card */ padding: 1px; /** diff --git a/src/styles/_assessment.scss b/src/styles/_assessment.scss index 4cbdfba7d3..2749f8bf1a 100644 --- a/src/styles/_assessment.scss +++ b/src/styles/_assessment.scss @@ -1,20 +1,21 @@ -.Assessment { +.AssessmentWorkspace { display: flex; flex-direction: column; height: 100%; } -.mission-briefing { +.assessment-briefing { padding-bottom: 0; - .mission-briefing-button { + .assessment-briefing-button { margin-top: 1rem; } } -.AssessmentListing { +.Assessment { color: $cadet-color-3; .listing { background-color: $cadet-color-5; + margin: 0px; text-align: justify; } }