Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b024ae3
Added react-md -- a Markdown editor
remo5000 Jul 4, 2018
5275bdb
Add draft-js dependency
remo5000 Jul 4, 2018
7fa8ef0
Add types for draft-js
remo5000 Jul 4, 2018
7cd2f97
Add showdown dependency
remo5000 Jul 4, 2018
a69951e
Add awesomefonts dependency
remo5000 Jul 4, 2018
b45a7f9
Add typedefs for showdown
remo5000 Jul 4, 2018
ffb6eb3
Add GradingEditor component
remo5000 Jul 4, 2018
6eb14fb
Add react-mde styles into index.scss
remo5000 Jul 4, 2018
da96de0
Fix overflow for react-mde in side-content card
remo5000 Jul 4, 2018
8724bf1
Fix scrolling problems using css
remo5000 Jul 4, 2018
de7fc50
Add coloring styles
remo5000 Jul 4, 2018
0744368
Add dispatch and state props
remo5000 Jul 5, 2018
5cc14c2
Add container
remo5000 Jul 5, 2018
c4df92d
Add action and reducer
remo5000 Jul 5, 2018
9663b27
Fix compiler errors
remo5000 Jul 5, 2018
8d49ea2
Remove undefined union for gradingCommentsValue
remo5000 Jul 5, 2018
86bfa7f
Scaffold link component to render for grid
remo5000 Jul 5, 2018
759793d
Format and de-style NavLink
remo5000 Jul 5, 2018
e734b8c
Use undefined check
remo5000 Jul 5, 2018
a65ec55
Force non-null for markdown value
remo5000 Jul 5, 2018
ef89a07
Add componentWillMount for grading/index
remo5000 Jul 5, 2018
33064f5
Add props for updating ids
remo5000 Jul 5, 2018
cdf5b63
Add action
remo5000 Jul 5, 2018
b6fa3e1
Fix buggy componentWillMount functions
remo5000 Jul 5, 2018
78c76c0
Add defaultComments value
remo5000 Jul 5, 2018
e59a4e2
Format files
remo5000 Jul 5, 2018
9106590
Add NumericInput for GradingEditor
remo5000 Jul 5, 2018
91720dd
Add NumericInput and Button
remo5000 Jul 5, 2018
43efb67
Add local state property to track XP given
remo5000 Jul 5, 2018
7407d1d
Add update for grading XP
remo5000 Jul 5, 2018
e457bc1
Add action for saving grading input
remo5000 Jul 5, 2018
f11e3de
Add saga for saving grading
remo5000 Jul 5, 2018
861ff5a
Format files
remo5000 Jul 5, 2018
409e0cd
Use blueprintjs icons instead of fontawesome
remo5000 Jul 5, 2018
14196ff
Fix save button wrap issue
remo5000 Jul 5, 2018
705a2e3
Format files
remo5000 Jul 5, 2018
b401b49
Remove blank line
remo5000 Jul 5, 2018
2e52bdc
Add some comments
remo5000 Jul 5, 2018
4d563dd
Format comments
remo5000 Jul 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ag-grid-react": "^18.0.0",
"astring": "^1.3.0",
"common-tags": "^1.7.2",
"draft-js": "^0.10.5",
"flexboxgrid": "^6.3.1",
"flexboxgrid-helpers": "^1.1.3",
"lodash": "^4.17.10",
Expand All @@ -51,6 +52,7 @@
"react-dom": "^16.3.1",
"react-dom-factories": "^1.0.2",
"react-hotkeys": "^1.1.4",
"react-mde": "^5.6.0",
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
Expand All @@ -59,6 +61,7 @@
"redux": "^3.7.2",
"redux-mock-store": "^1.5.1",
"redux-saga": "^0.15.6",
"showdown": "^1.8.6",
"typesafe-actions": "^1.1.2",
"utility-types": "^2.0.0"
},
Expand All @@ -70,6 +73,7 @@
"@types/classnames": "^2.2.3",
"@types/common-tags": "^1.4.0",
"@types/dotenv": "^4.0.3",
"@types/draft-js": "^0.10.23",
"@types/enzyme": "^3.1.9",
"@types/enzyme-adapter-react-16": "^1.0.2",
"@types/estree": "^0.0.39",
Expand All @@ -89,6 +93,7 @@
"@types/react-router-redux": "^5.0.13",
"@types/react-test-renderer": "^16.0.1",
"@types/redux-mock-store": "^0.0.13",
"@types/showdown": "^1.7.5",
"babel-core": "6",
"babel-runtime": "^6.23.0",
"coveralls": "^3.0.1",
Expand Down
4 changes: 4 additions & 0 deletions src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export const UPDATE_REPL_VALUE = 'UPDATE_REPL_VALUE'
export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT'
export const RESET_ASSESSMENT_WORKSPACE = 'RESET_ASSESSMENT_WORKSPACE'
export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'
export const UPDATE_CURRENT_SUBMISSION_ID = 'UPDATE_CURRENT_SUBMISSION_ID'
export const UPDATE_GRADING_COMMENTS_VALUE = 'UPDATE_GRADING_COMMENTS_VALUE'
export const UPDATE_GRADING_XP = 'UPDATE_GRADING_XP'
export const SAVE_GRADING_INPUT = 'SAVE_GRADING_INPUT'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I change this to UPLOAD_GRADING_INPUT instead? It's only use is to call a saga.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think save is fine


/** Session */
export const FETCH_ANNOUNCEMENTS = 'FETCH_ANNOUNCEMENTS'
Expand Down
31 changes: 31 additions & 0 deletions src/actions/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,34 @@ export const updateCurrentAssessmentId = (assessmentId: number, questionId: numb
questionId
}
})

export const updateCurrentSubmissionId = (submissionId: number, questionId: number) => ({
type: actionTypes.UPDATE_CURRENT_SUBMISSION_ID,
payload: {
submissionId,
questionId
}
})

export const updateGradingCommentsValue: ActionCreator<actionTypes.IAction> = (
newComments: string
) => ({
type: actionTypes.UPDATE_GRADING_COMMENTS_VALUE,
payload: newComments
})

export const updateGradingXP: ActionCreator<actionTypes.IAction> = (newXP: number) => ({
type: actionTypes.UPDATE_GRADING_XP,
payload: newXP
})

export const saveGradingInput: ActionCreator<actionTypes.IAction> = (
gradingCommentsValue: string,
gradingXP: number | undefined
) => ({
type: actionTypes.SAVE_GRADING_INPUT,
payload: {
gradingCommentsValue,
gradingXP
}
})
162 changes: 162 additions & 0 deletions src/components/academy/grading/GradingEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { ButtonGroup, Icon, NumericInput, Position } from '@blueprintjs/core'
import { IconNames } from '@blueprintjs/icons'
import * as React from 'react'
import ReactMde, { ReactMdeTypes } from 'react-mde'
import * as Showdown from 'showdown'
import { controlButton } from '../../commons'

type GradingEditorProps = DispatchProps & OwnProps & StateProps

export type DispatchProps = {
handleGradingCommentsChange: (s: string) => void
handleGradingXPChange: (i: number | undefined) => void
handleGradingInputSave: (s: string, i: number | undefined) => void
}

export type OwnProps = {
maximumXP: number
}

export type StateProps = {
gradingCommentsValue: string
gradingXP: number | undefined
}

/**
* Keeps track of the current editor state,
* as well as the XP in the numeric input.
*
* XP can be undefined to show the hint text.
*/
type State = {
mdeState: ReactMdeTypes.MdeState
XPInput: number | undefined
}

class GradingEditor extends React.Component<GradingEditorProps, State> {
private converter: Showdown.Converter

constructor(props: GradingEditorProps) {
super(props)
this.state = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local state is used to keep track of everything here, because the editor already uses it. Upon unmounting, the values are dispatched.

mdeState: {
markdown: this.props.gradingCommentsValue
},
XPInput: this.props.gradingXP
}
/**
* The markdown-to-html converter for the editor.
*/
this.converter = new Showdown.Converter({
tables: true,
simplifiedAutoLink: true,
strikethrough: true,
tasklists: true,
openLinksInNewWindow: true
})
}

/**
* Update the redux state's grading comments value, using the latest
* value in the local state.
*/
public componentWillUnmount() {
this.props.handleGradingCommentsChange(this.state.mdeState.markdown!)
this.props.handleGradingXPChange(this.state.XPInput)
}

public render() {
return (
<>
<div className="grading-editor-input-parent">
<ButtonGroup fill={true}>
<NumericInput
onValueChange={this.onXPInputChange}
value={this.state.XPInput}
buttonPosition={Position.LEFT}
placeholder="XP here"
min={0}
max={this.props.maximumXP}
/>
{controlButton('Save', IconNames.FLOPPY_DISK, this.onClickSaveButton)}
</ButtonGroup>
</div>
<div className="react-mde-parent">
<ReactMde
buttonContentOptions={{
iconProvider: this.blueprintIconProvider
}}
layout={'vertical'}
onChange={this.handleValueChange}
editorState={this.state.mdeState}
generateMarkdownPreview={this.generateMarkdownPreview}
/>
</div>
</>
)
}

/**
* A custom icons provider. It uses a bulky mapping function
* defined below.
*
* See {@link https://github.com/andrerpena/react-mde}
*/
private blueprintIconProvider(name: string) {
return <Icon icon={faToBlueprintIconMapping(name)} />
}

private onClickSaveButton = () => {
this.props.handleGradingInputSave(this.state.mdeState.markdown!, this.state.XPInput)
}

private onXPInputChange = (newValue: number) => {
this.setState({
...this.state,
XPInput: newValue
})
}

private handleValueChange = (mdeState: ReactMdeTypes.MdeState) => {
this.setState({ mdeState })
}

private generateMarkdownPreview = (markdown: string) =>
Promise.resolve(this.converter.makeHtml(markdown))
}

/**
* Maps FontAwesome5 icon names to blueprintjs counterparts.
* This is to reduce the number of dependencies on icons, and
* keep a more consistent look.
*/
const faToBlueprintIconMapping = (name: string) => {
switch (name) {
case 'heading':
return IconNames.HEADER
case 'bold':
return IconNames.BOLD
case 'italic':
return IconNames.ITALIC
case 'strikethrough':
return IconNames.STRIKETHROUGH
case 'link':
return IconNames.LINK
case 'quote-right':
return IconNames.CITATION
case 'code':
return IconNames.CODE
case 'image':
return IconNames.MEDIA
case 'list-ul':
return IconNames.PROPERTIES
case 'list-ol':
return IconNames.NUMBERED_LIST
case 'tasks':
return IconNames.TICK
default:
return IconNames.HELP
}
}

export default GradingEditor
31 changes: 31 additions & 0 deletions src/components/academy/grading/GradingNavLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react'
import { NavLink } from 'react-router-dom'

import { GradingOverview } from './gradingShape'

type GradingNavLinkProps = {
data: GradingOverview
}

/**
* Used to render a link in the table that displays GradingOverviews.
* This is a fully fledged component (not SFC) by specification in
* ag-grid.
*
* See {@link https://www.ag-grid.com/example-react-dynamic}
*/
class GradingNavLink extends React.Component<GradingNavLinkProps, {}> {
constructor(props: GradingNavLinkProps) {
super(props)
}

public render() {
return (
<NavLink to={`/academy/grading/${this.props.data.submissionId}`} activeClassName="pt-active">
{this.props.data.graded ? 'Done' : 'Not Graded'}
</NavLink>
)
}
}

export default GradingNavLink
6 changes: 3 additions & 3 deletions src/components/academy/grading/GradingWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NonIdealState, Spinner, Text } from '@blueprintjs/core'
import { IconNames } from '@blueprintjs/icons'
import * as React from 'react'

import GradingEditor from '../../../containers/academy/grading/GradingEditorContainer'
import { InterpreterOutput } from '../../../reducers/states'
import { history } from '../../../utils/history'
import {
Expand Down Expand Up @@ -108,7 +109,8 @@ class GradingWorkspace extends React.Component<GradingWorkspaceProps> {
{
label: `Grading: Question ${props.questionId}`,
icon: IconNames.TICK,
body: this.gradingTab(props)
/* Render an editor with the xp given to the current question. */
body: <GradingEditor maximumXP={props.grading![props.questionId].maximumXP} />
},
{
label: `Task ${props.questionId}`,
Expand Down Expand Up @@ -145,8 +147,6 @@ class GradingWorkspace extends React.Component<GradingWorkspaceProps> {
sourceChapter: 2 // TODO dynamic library changing
}
}

private gradingTab = (props: GradingWorkspaceProps) => <h2> Grading </h2>
}

export default GradingWorkspace
31 changes: 26 additions & 5 deletions src/components/academy/grading/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import GradingWorkspaceContainer from '../../../containers/academy/grading/Gradi
import { stringParamToInt } from '../../../utils/paramParseHelpers'
import { controlButton } from '../../commons'
import ContentDisplay from '../../commons/ContentDisplay'
import GradingNavLink from './GradingNavLink'
import { GradingOverview } from './gradingShape'
import { OwnProps as GradingWorkspaceProps } from './GradingWorkspace'

Expand All @@ -34,10 +35,14 @@ export interface IGradingWorkspaceParams {

export interface IDispatchProps {
handleFetchGradingOverviews: () => void
handleUpdateCurrentSubmissionId: (submissionId: number, questionId: number) => void
handleResetAssessmentWorkspace: () => void
}

export interface IStateProps {
gradingOverviews?: GradingOverview[]
storedSubmissionId?: number
storedQuestionId?: number
}

class Grading extends React.Component<IGradingProps, State> {
Expand All @@ -60,16 +65,32 @@ class Grading extends React.Component<IGradingProps, State> {
{
headerName: 'Graded',
field: 'graded',
cellRenderer: ({ data }: { data: GradingOverview }) => {
return `<a href='${window.location.origin}/academy/grading/${data.submissionId}'>${
data.graded ? 'Done' : 'Not Graded'
}</a>`
}
cellRendererFramework: GradingNavLink
}
]
}
}

/**
* If the current SubmissionId/QuestionId has changed, update it
* in the store and reset the workspace.
*/
public componentWillMount() {
const submissionId = stringParamToInt(this.props.match.params.submissionId)
if (submissionId === null) {
return
}
const questionId = stringParamToInt(this.props.match.params.questionId)!

if (
this.props.storedSubmissionId !== submissionId ||
this.props.storedQuestionId !== questionId
) {
this.props.handleUpdateCurrentSubmissionId(submissionId, questionId)
this.props.handleResetAssessmentWorkspace()
}
}

public render() {
const submissionId: number | null = stringParamToInt(this.props.match.params.submissionId)
// default questionId is 0 (the first question)
Expand Down
4 changes: 2 additions & 2 deletions src/components/assessment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class Assessment extends React.Component<IAssessmentProps, {}> {
*/
public componentWillMount() {
const assessmentId = stringParamToInt(this.props.match.params.assessmentId)
const questionId = stringParamToInt(this.props.match.params.questionId)
if (assessmentId === null || questionId === null) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a bug that applies for Grading but not Assessment. I changed it for consistency.

The original predicate fails when questionid is null but submissionId is non-null (because we have not assigned 0 to it yet). In the case of assessments, the 0 value is provided by the card, since it must provide a value in the link within.

if (assessmentId === null) {
return
}
const questionId = stringParamToInt(this.props.match.params.questionId)!

if (
this.props.storedAssessmentId !== assessmentId ||
Expand Down
Loading