Skip to content

Commit 80482b4

Browse files
authored
Grading comments (#162)
* Added react-md -- a Markdown editor - ~400 stars - In Typescript - Vertical layout for the slightly narrow side-content section - In actual Markdown Only caveats are that it is not WYSIWYG and does not have a markdown to html converter. * Add draft-js dependency The main library react-mde runs on. * Add types for draft-js * Add showdown dependency To convert markdown to HTML and vice-versa * Add awesomefonts dependency I'll remove this to use blueprint icons instead, later on. * Add typedefs for showdown * Add GradingEditor component * Add react-mde styles into index.scss Or else, the styles do not work. * Fix overflow for react-mde in side-content card * Fix scrolling problems using css * Add coloring styles * Add dispatch and state props Trying to use componentDidMount and componentWillUnmount to handle updates. * Add container * Add action and reducer * Fix compiler errors Alphabetisizing, typos. * Remove undefined union for gradingCommentsValue The `markdown` value in `mdeState` being undefined causes a runtime error. So it is easier to just deal with a definite string (since undefined is not allowed anyway). The typedef for the value allows undefined though. * Scaffold link component to render for grid The problem is that the current <a href> implementation is a hard redirect, causing a change in URL (and loss of all state). I'm trying to get a NavLink * Format and de-style NavLink * Use undefined check * Force non-null for markdown value Since it will be set by the default state, it is never null. * Add componentWillMount for grading/index * Add props for updating ids * Add action * Fix buggy componentWillMount functions Checks only for submissionId, if it is present then there should be a questionId as well. * Add defaultComments value Used to reset the workspace as well. * Format files * Add NumericInput for GradingEditor * Add NumericInput and Button * Add local state property to track XP given * Add update for grading XP * Add action for saving grading input * Add saga for saving grading * Format files * Use blueprintjs icons instead of fontawesome * Fix save button wrap issue * Format files * Remove blank line * Add some comments * Format comments
1 parent 392c6ba commit 80482b4

File tree

16 files changed

+1464
-60
lines changed

16 files changed

+1464
-60
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"ag-grid-react": "^18.0.0",
3535
"astring": "^1.3.0",
3636
"common-tags": "^1.7.2",
37+
"draft-js": "^0.10.5",
3738
"flexboxgrid": "^6.3.1",
3839
"flexboxgrid-helpers": "^1.1.3",
3940
"lodash": "^4.17.10",
@@ -51,6 +52,7 @@
5152
"react-dom": "^16.3.1",
5253
"react-dom-factories": "^1.0.2",
5354
"react-hotkeys": "^1.1.4",
55+
"react-mde": "^5.6.0",
5456
"react-redux": "^5.0.7",
5557
"react-router": "^4.2.0",
5658
"react-router-dom": "^4.2.2",
@@ -59,6 +61,7 @@
5961
"redux": "^3.7.2",
6062
"redux-mock-store": "^1.5.1",
6163
"redux-saga": "^0.15.6",
64+
"showdown": "^1.8.6",
6265
"typesafe-actions": "^1.1.2",
6366
"utility-types": "^2.0.0"
6467
},
@@ -70,6 +73,7 @@
7073
"@types/classnames": "^2.2.3",
7174
"@types/common-tags": "^1.4.0",
7275
"@types/dotenv": "^4.0.3",
76+
"@types/draft-js": "^0.10.23",
7377
"@types/enzyme": "^3.1.9",
7478
"@types/enzyme-adapter-react-16": "^1.0.2",
7579
"@types/estree": "^0.0.39",
@@ -89,6 +93,7 @@
8993
"@types/react-router-redux": "^5.0.13",
9094
"@types/react-test-renderer": "^16.0.1",
9195
"@types/redux-mock-store": "^0.0.13",
96+
"@types/showdown": "^1.7.5",
9297
"babel-core": "6",
9398
"babel-runtime": "^6.23.0",
9499
"coveralls": "^3.0.1",

src/actions/actionTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export const UPDATE_REPL_VALUE = 'UPDATE_REPL_VALUE'
3333
export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT'
3434
export const RESET_ASSESSMENT_WORKSPACE = 'RESET_ASSESSMENT_WORKSPACE'
3535
export const UPDATE_CURRENT_ASSESSMENT_ID = 'UPDATE_CURRENT_ASSESSMENT_ID'
36+
export const UPDATE_CURRENT_SUBMISSION_ID = 'UPDATE_CURRENT_SUBMISSION_ID'
37+
export const UPDATE_GRADING_COMMENTS_VALUE = 'UPDATE_GRADING_COMMENTS_VALUE'
38+
export const UPDATE_GRADING_XP = 'UPDATE_GRADING_XP'
39+
export const SAVE_GRADING_INPUT = 'SAVE_GRADING_INPUT'
3640

3741
/** Session */
3842
export const FETCH_ANNOUNCEMENTS = 'FETCH_ANNOUNCEMENTS'

src/actions/workspaces.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,34 @@ export const updateCurrentAssessmentId = (assessmentId: number, questionId: numb
125125
questionId
126126
}
127127
})
128+
129+
export const updateCurrentSubmissionId = (submissionId: number, questionId: number) => ({
130+
type: actionTypes.UPDATE_CURRENT_SUBMISSION_ID,
131+
payload: {
132+
submissionId,
133+
questionId
134+
}
135+
})
136+
137+
export const updateGradingCommentsValue: ActionCreator<actionTypes.IAction> = (
138+
newComments: string
139+
) => ({
140+
type: actionTypes.UPDATE_GRADING_COMMENTS_VALUE,
141+
payload: newComments
142+
})
143+
144+
export const updateGradingXP: ActionCreator<actionTypes.IAction> = (newXP: number) => ({
145+
type: actionTypes.UPDATE_GRADING_XP,
146+
payload: newXP
147+
})
148+
149+
export const saveGradingInput: ActionCreator<actionTypes.IAction> = (
150+
gradingCommentsValue: string,
151+
gradingXP: number | undefined
152+
) => ({
153+
type: actionTypes.SAVE_GRADING_INPUT,
154+
payload: {
155+
gradingCommentsValue,
156+
gradingXP
157+
}
158+
})
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { ButtonGroup, Icon, NumericInput, Position } from '@blueprintjs/core'
2+
import { IconNames } from '@blueprintjs/icons'
3+
import * as React from 'react'
4+
import ReactMde, { ReactMdeTypes } from 'react-mde'
5+
import * as Showdown from 'showdown'
6+
import { controlButton } from '../../commons'
7+
8+
type GradingEditorProps = DispatchProps & OwnProps & StateProps
9+
10+
export type DispatchProps = {
11+
handleGradingCommentsChange: (s: string) => void
12+
handleGradingXPChange: (i: number | undefined) => void
13+
handleGradingInputSave: (s: string, i: number | undefined) => void
14+
}
15+
16+
export type OwnProps = {
17+
maximumXP: number
18+
}
19+
20+
export type StateProps = {
21+
gradingCommentsValue: string
22+
gradingXP: number | undefined
23+
}
24+
25+
/**
26+
* Keeps track of the current editor state,
27+
* as well as the XP in the numeric input.
28+
*
29+
* XP can be undefined to show the hint text.
30+
*/
31+
type State = {
32+
mdeState: ReactMdeTypes.MdeState
33+
XPInput: number | undefined
34+
}
35+
36+
class GradingEditor extends React.Component<GradingEditorProps, State> {
37+
private converter: Showdown.Converter
38+
39+
constructor(props: GradingEditorProps) {
40+
super(props)
41+
this.state = {
42+
mdeState: {
43+
markdown: this.props.gradingCommentsValue
44+
},
45+
XPInput: this.props.gradingXP
46+
}
47+
/**
48+
* The markdown-to-html converter for the editor.
49+
*/
50+
this.converter = new Showdown.Converter({
51+
tables: true,
52+
simplifiedAutoLink: true,
53+
strikethrough: true,
54+
tasklists: true,
55+
openLinksInNewWindow: true
56+
})
57+
}
58+
59+
/**
60+
* Update the redux state's grading comments value, using the latest
61+
* value in the local state.
62+
*/
63+
public componentWillUnmount() {
64+
this.props.handleGradingCommentsChange(this.state.mdeState.markdown!)
65+
this.props.handleGradingXPChange(this.state.XPInput)
66+
}
67+
68+
public render() {
69+
return (
70+
<>
71+
<div className="grading-editor-input-parent">
72+
<ButtonGroup fill={true}>
73+
<NumericInput
74+
onValueChange={this.onXPInputChange}
75+
value={this.state.XPInput}
76+
buttonPosition={Position.LEFT}
77+
placeholder="XP here"
78+
min={0}
79+
max={this.props.maximumXP}
80+
/>
81+
{controlButton('Save', IconNames.FLOPPY_DISK, this.onClickSaveButton)}
82+
</ButtonGroup>
83+
</div>
84+
<div className="react-mde-parent">
85+
<ReactMde
86+
buttonContentOptions={{
87+
iconProvider: this.blueprintIconProvider
88+
}}
89+
layout={'vertical'}
90+
onChange={this.handleValueChange}
91+
editorState={this.state.mdeState}
92+
generateMarkdownPreview={this.generateMarkdownPreview}
93+
/>
94+
</div>
95+
</>
96+
)
97+
}
98+
99+
/**
100+
* A custom icons provider. It uses a bulky mapping function
101+
* defined below.
102+
*
103+
* See {@link https://github.com/andrerpena/react-mde}
104+
*/
105+
private blueprintIconProvider(name: string) {
106+
return <Icon icon={faToBlueprintIconMapping(name)} />
107+
}
108+
109+
private onClickSaveButton = () => {
110+
this.props.handleGradingInputSave(this.state.mdeState.markdown!, this.state.XPInput)
111+
}
112+
113+
private onXPInputChange = (newValue: number) => {
114+
this.setState({
115+
...this.state,
116+
XPInput: newValue
117+
})
118+
}
119+
120+
private handleValueChange = (mdeState: ReactMdeTypes.MdeState) => {
121+
this.setState({ mdeState })
122+
}
123+
124+
private generateMarkdownPreview = (markdown: string) =>
125+
Promise.resolve(this.converter.makeHtml(markdown))
126+
}
127+
128+
/**
129+
* Maps FontAwesome5 icon names to blueprintjs counterparts.
130+
* This is to reduce the number of dependencies on icons, and
131+
* keep a more consistent look.
132+
*/
133+
const faToBlueprintIconMapping = (name: string) => {
134+
switch (name) {
135+
case 'heading':
136+
return IconNames.HEADER
137+
case 'bold':
138+
return IconNames.BOLD
139+
case 'italic':
140+
return IconNames.ITALIC
141+
case 'strikethrough':
142+
return IconNames.STRIKETHROUGH
143+
case 'link':
144+
return IconNames.LINK
145+
case 'quote-right':
146+
return IconNames.CITATION
147+
case 'code':
148+
return IconNames.CODE
149+
case 'image':
150+
return IconNames.MEDIA
151+
case 'list-ul':
152+
return IconNames.PROPERTIES
153+
case 'list-ol':
154+
return IconNames.NUMBERED_LIST
155+
case 'tasks':
156+
return IconNames.TICK
157+
default:
158+
return IconNames.HELP
159+
}
160+
}
161+
162+
export default GradingEditor
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from 'react'
2+
import { NavLink } from 'react-router-dom'
3+
4+
import { GradingOverview } from './gradingShape'
5+
6+
type GradingNavLinkProps = {
7+
data: GradingOverview
8+
}
9+
10+
/**
11+
* Used to render a link in the table that displays GradingOverviews.
12+
* This is a fully fledged component (not SFC) by specification in
13+
* ag-grid.
14+
*
15+
* See {@link https://www.ag-grid.com/example-react-dynamic}
16+
*/
17+
class GradingNavLink extends React.Component<GradingNavLinkProps, {}> {
18+
constructor(props: GradingNavLinkProps) {
19+
super(props)
20+
}
21+
22+
public render() {
23+
return (
24+
<NavLink to={`/academy/grading/${this.props.data.submissionId}`} activeClassName="pt-active">
25+
{this.props.data.graded ? 'Done' : 'Not Graded'}
26+
</NavLink>
27+
)
28+
}
29+
}
30+
31+
export default GradingNavLink

src/components/academy/grading/GradingWorkspace.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NonIdealState, Spinner, Text } from '@blueprintjs/core'
22
import { IconNames } from '@blueprintjs/icons'
33
import * as React from 'react'
44

5+
import GradingEditor from '../../../containers/academy/grading/GradingEditorContainer'
56
import { InterpreterOutput } from '../../../reducers/states'
67
import { history } from '../../../utils/history'
78
import {
@@ -108,7 +109,8 @@ class GradingWorkspace extends React.Component<GradingWorkspaceProps> {
108109
{
109110
label: `Grading: Question ${props.questionId}`,
110111
icon: IconNames.TICK,
111-
body: this.gradingTab(props)
112+
/* Render an editor with the xp given to the current question. */
113+
body: <GradingEditor maximumXP={props.grading![props.questionId].maximumXP} />
112114
},
113115
{
114116
label: `Task ${props.questionId}`,
@@ -145,8 +147,6 @@ class GradingWorkspace extends React.Component<GradingWorkspaceProps> {
145147
sourceChapter: 2 // TODO dynamic library changing
146148
}
147149
}
148-
149-
private gradingTab = (props: GradingWorkspaceProps) => <h2> Grading </h2>
150150
}
151151

152152
export default GradingWorkspace

src/components/academy/grading/index.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import GradingWorkspaceContainer from '../../../containers/academy/grading/Gradi
1111
import { stringParamToInt } from '../../../utils/paramParseHelpers'
1212
import { controlButton } from '../../commons'
1313
import ContentDisplay from '../../commons/ContentDisplay'
14+
import GradingNavLink from './GradingNavLink'
1415
import { GradingOverview } from './gradingShape'
1516
import { OwnProps as GradingWorkspaceProps } from './GradingWorkspace'
1617

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

3536
export interface IDispatchProps {
3637
handleFetchGradingOverviews: () => void
38+
handleUpdateCurrentSubmissionId: (submissionId: number, questionId: number) => void
39+
handleResetAssessmentWorkspace: () => void
3740
}
3841

3942
export interface IStateProps {
4043
gradingOverviews?: GradingOverview[]
44+
storedSubmissionId?: number
45+
storedQuestionId?: number
4146
}
4247

4348
class Grading extends React.Component<IGradingProps, State> {
@@ -60,16 +65,32 @@ class Grading extends React.Component<IGradingProps, State> {
6065
{
6166
headerName: 'Graded',
6267
field: 'graded',
63-
cellRenderer: ({ data }: { data: GradingOverview }) => {
64-
return `<a href='${window.location.origin}/academy/grading/${data.submissionId}'>${
65-
data.graded ? 'Done' : 'Not Graded'
66-
}</a>`
67-
}
68+
cellRendererFramework: GradingNavLink
6869
}
6970
]
7071
}
7172
}
7273

74+
/**
75+
* If the current SubmissionId/QuestionId has changed, update it
76+
* in the store and reset the workspace.
77+
*/
78+
public componentWillMount() {
79+
const submissionId = stringParamToInt(this.props.match.params.submissionId)
80+
if (submissionId === null) {
81+
return
82+
}
83+
const questionId = stringParamToInt(this.props.match.params.questionId)!
84+
85+
if (
86+
this.props.storedSubmissionId !== submissionId ||
87+
this.props.storedQuestionId !== questionId
88+
) {
89+
this.props.handleUpdateCurrentSubmissionId(submissionId, questionId)
90+
this.props.handleResetAssessmentWorkspace()
91+
}
92+
}
93+
7394
public render() {
7495
const submissionId: number | null = stringParamToInt(this.props.match.params.submissionId)
7596
// default questionId is 0 (the first question)

src/components/assessment/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ class Assessment extends React.Component<IAssessmentProps, {}> {
4444
*/
4545
public componentWillMount() {
4646
const assessmentId = stringParamToInt(this.props.match.params.assessmentId)
47-
const questionId = stringParamToInt(this.props.match.params.questionId)
48-
if (assessmentId === null || questionId === null) {
47+
if (assessmentId === null) {
4948
return
5049
}
50+
const questionId = stringParamToInt(this.props.match.params.questionId)!
5151

5252
if (
5353
this.props.storedAssessmentId !== assessmentId ||

0 commit comments

Comments
 (0)