From c4fe884eb6ce95d6494752c3151642214c81eb2b Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 10:59:04 +0800 Subject: [PATCH 01/26] Removed previous button on first question, added divider --- src/components/incubator/EditingWorkspace.tsx | 52 +++++++++++-------- .../editingWorkspaceSideContent/HelpTab.tsx | 31 +++++++++++ .../ManageQuestionTab.tsx | 6 +-- .../TextareaContent.tsx | 1 - src/components/workspace/ControlBar.tsx | 2 +- 5 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 8e4ced7835..a6e5a83c02 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -30,6 +30,7 @@ import { QuestionTemplateTab, TextareaContentTab } from './editingWorkspaceSideContent'; +import HelpTab from './editingWorkspaceSideContent/HelpTab'; export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; @@ -288,58 +289,65 @@ class AssessmentWorkspace extends React.Component ) }, { - label: `${assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, + label: `Add/Delete Questions`, + icon: IconNames.WRENCH, body: ( - ) }, { - label: `Question Template`, - icon: IconNames.DOCUMENT, + label: `Manage Global Deployment`, + icon: IconNames.GLOBE, body: ( - ) }, { - label: `Manage Question`, - icon: IconNames.WRENCH, + label: `Help`, + icon: IconNames.HELP, body: ( - + ) + }, + { + label: `Task ${questionId + 1}`, + icon: IconNames.NINJA, + body: ( + ) }, { - label: `Manage Global Deployment`, - icon: IconNames.GLOBE, + label: `Question Template`, + icon: IconNames.DOCUMENT, body: ( - ) }, diff --git a/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx new file mode 100644 index 0000000000..7863b6e9af --- /dev/null +++ b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx @@ -0,0 +1,31 @@ +// import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + +export class HelpTab extends React.Component<{}, {}> { + public constructor(props: {}) { + super(props); + } + + public render() { + return this.helpTab(); + } + + private helpTab = () => { + return ( +
+ Mission-specific tabs:
+ Modify mission brief in the ission Briefing tab by clicking on the text
+ Insert questions at the current question index or delete the current question in the Add/Delete Questions tab
+ The Source version, library and globals for all questions in the mission can be modified in Global Deployment
+
+ Question-specific tabs:
+ Modify question description in Task tab
+ Modify question template in Question template tab
+ The Source version, library and globals for the current question can be modified in Global Deployment
+ Modify grading in Grading tab
+
+ ); + }; +} + +export default HelpTab; diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index 043a646d72..4bfed79c6c 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -24,12 +24,12 @@ export class ManageQuestionTab extends React.Component { return (
{controlButton( - 'Make Programming Question', + 'Insert Programming Question', IconNames.FONT, this.makeQuestion(programmingTemplate) )} - {controlButton('Make MCQ Question', IconNames.CONFIRM, this.makeQuestion(mcqTemplate))} - {controlButton('Make Programming Question', IconNames.REMOVE, this.deleteQn)} + {controlButton('Insert MCQ Question', IconNames.CONFIRM, this.makeQuestion(mcqTemplate))} + {controlButton('Delete Current Question', IconNames.REMOVE, this.deleteQn)}
); }; diff --git a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx index 9d84982776..7d98de8b81 100644 --- a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx @@ -12,7 +12,6 @@ interface IProps { path: Array; processResults?: (str: string | number) => string | number; useRawValue?: boolean; - processResults?: (newVal: string | number) => string | number; updateAssessment: (assessment: IAssessment) => void; } diff --git a/src/components/workspace/ControlBar.tsx b/src/components/workspace/ControlBar.tsx index 5bf80be90e..899b15d08b 100755 --- a/src/components/workspace/ControlBar.tsx +++ b/src/components/workspace/ControlBar.tsx @@ -201,7 +201,7 @@ class ControlBar extends React.PureComponent { } private hasPreviousButton() { - return this.props.questionProgress && this.props.questionProgress[0] > 0; + return this.props.questionProgress && this.props.questionProgress[0] > 1; } private hasReturnButton() { From 15d87493a9604814e0a5b7fad0faa9a5959761ee Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 11:00:24 +0800 Subject: [PATCH 02/26] Yarn format --- src/components/incubator/EditingWorkspace.tsx | 4 +--- .../editingWorkspaceSideContent/HelpTab.tsx | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index a6e5a83c02..4f01c92a6d 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -325,9 +325,7 @@ class AssessmentWorkspace extends React.Component - ) + body: }, { label: `Task ${questionId + 1}`, diff --git a/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx index 7863b6e9af..9ee97b33c8 100644 --- a/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx @@ -13,16 +13,19 @@ export class HelpTab extends React.Component<{}, {}> { private helpTab = () => { return (
- Mission-specific tabs:
- Modify mission brief in the ission Briefing tab by clicking on the text
- Insert questions at the current question index or delete the current question in the Add/Delete Questions tab
- The Source version, library and globals for all questions in the mission can be modified in Global Deployment
-
- Question-specific tabs:
- Modify question description in Task tab
- Modify question template in Question template tab
- The Source version, library and globals for the current question can be modified in Global Deployment
- Modify grading in Grading tab
+ Mission-specific tabs:
+ Modify mission brief in the ission Briefing tab by clicking on the text
+ Insert questions at the current question index or delete the current question in the + Add/Delete Questions tab
+ The Source version, library and globals for all questions in the mission can be modified in + Global Deployment
+
+ Question-specific tabs:
+ Modify question description in Task tab
+ Modify question template in Question template tab
+ The Source version, library and globals for the current question can be modified in Global + Deployment
+ Modify grading in Grading tab
); }; From 079b843c96abfbe6ce415adc7bfd8462f2814a1a Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 11:04:02 +0800 Subject: [PATCH 03/26] Changeded help description --- .../editingWorkspaceSideContent/HelpTab.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx index 9ee97b33c8..31d1e68e39 100644 --- a/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/HelpTab.tsx @@ -14,18 +14,15 @@ export class HelpTab extends React.Component<{}, {}> { return (
Mission-specific tabs:
- Modify mission brief in the ission Briefing tab by clicking on the text
- Insert questions at the current question index or delete the current question in the - Add/Delete Questions tab
- The Source version, library and globals for all questions in the mission can be modified in - Global Deployment
+ Mission Briefing: Modify mission brief by clicking on the text
+ Add/Delete Questions: Insert questions at the current question index or delete the current question
+ Global Deployment: Modify the Source version, library and globals for all questions in the mission

Question-specific tabs:
- Modify question description in Task tab
- Modify question template in Question template tab
- The Source version, library and globals for the current question can be modified in Global - Deployment
- Modify grading in Grading tab
+ Task: Modify question descriptio
+ Question Template: Modify question template in Question template tab
+ Local Deployment: Modify the Source version, library and globals for the current question
+ Grading: Modify grading
); }; From f09f97c9a20d165403415cfaec44c89277c475cf Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Sun, 24 Mar 2019 12:15:06 +0800 Subject: [PATCH 04/26] fix import for firefox --- .../TextareaContent.tsx | 20 +++++++++---------- src/utils/xmlParser.ts | 19 +++++++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx index 27a0f9a4ea..613b477cb8 100644 --- a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx @@ -35,20 +35,20 @@ export class TextareaContent extends React.Component { } public render() { - const filler = 'Please enter value (if applicable)'; let display; if (this.state.isEditing) { - display =
{this.makeEditingTextarea()}
; + display = this.makeEditingTextarea(); } else { - let value = getValueFromPath(this.props.path, this.props.assessment) || filler; - value = value.match('^(\n| )*$') ? filler : value; - display = ( -
- {this.state.useRawValue ? value : } -
- ); + if (this.state.useRawValue) { + display = getValueFromPath(this.props.path, this.props.assessment); + } else { + const filler = 'Please enter value (if applicable)'; + let value = getValueFromPath(this.props.path, this.props.assessment) || ''; + value = value.match('^(\n| )*$') ? filler : value; + display = ; + } } - return display; + return
{display}
; } private saveEditAssessment = (e: any) => { diff --git a/src/utils/xmlParser.ts b/src/utils/xmlParser.ts index cbe827acee..54765a77ce 100644 --- a/src/utils/xmlParser.ts +++ b/src/utils/xmlParser.ts @@ -237,14 +237,23 @@ export const exportXml = () => { }; let xmlStr = builder.buildObject(xml); xmlStr = xmlStr.replace(/( )+/g, ''); - const element = document.createElement('a'); - const file = new Blob([xmlStr], { endings: 'native', type: 'text/xml;charset=UTF-8' }); - element.href = URL.createObjectURL(file); - element.download = title + '.xml'; - element.click(); + download(title + '.xml', xmlStr); } }; +const download = (filename: string, text: string) => { + const element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +}; + const exportLibrary = (library: Library) => { const deployment = { $: { From 0547d1fa076dd4eebd212127d7056f988562a166 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 13:39:30 +0800 Subject: [PATCH 05/26] max width for globals --- .../incubator/assessmentTemplates.ts | 2 +- .../DeploymentTab.tsx | 13 ++++++--- .../ManageQuestionTab.tsx | 6 ++--- .../QuestionTemplateTab.tsx | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/components/incubator/assessmentTemplates.ts b/src/components/incubator/assessmentTemplates.ts index e8684115a3..10a77e5e8c 100644 --- a/src/components/incubator/assessmentTemplates.ts +++ b/src/components/incubator/assessmentTemplates.ts @@ -97,7 +97,7 @@ export const mcqTemplate = (): IMCQQuestion => { id: 2, library: emptyLibrary(), type: 'mcq', - solution: 1, + solution: 0, grader: { name: 'avenger', id: 1 diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index e762dc80da..26f99f96de 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -65,12 +65,19 @@ export class DeploymentTab extends React.Component{controlButton('Delete', IconNames.MINUS, this.handleSymbolDelete(i))} )); + const globals = deployment.globals.map((symbol, i) => ( - - {this.textareaContent(deploymentPath.concat(['globals', i, 0]))} + +
+ {this.textareaContent(deploymentPath.concat(['globals', i, 0]))} +
+ + +
+ {this.globalValueTextareaContent(i)} +
- {this.globalValueTextareaContent(i)} {controlButton('Delete', IconNames.MINUS, this.handleGlobalDelete(i))} diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index 15cf713006..5674e7f453 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -24,12 +24,12 @@ export class ManageQuestionTab extends React.Component { return (
{controlButton( - 'Make Programming Question', + 'Insert Programming Question', IconNames.FONT, this.makeQuestion(programmingTemplate) )} - {controlButton('Make MCQ Question', IconNames.CONFIRM, this.makeQuestion(mcqTemplate))} - {controlButton('Delete Question', IconNames.REMOVE, this.deleteQn)} + {controlButton('Insert MCQ Question', IconNames.CONFIRM, this.makeQuestion(mcqTemplate))} + {controlButton('Delete Current Question', IconNames.REMOVE, this.deleteQn)}
); }; diff --git a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx index cfe5a3b9d8..2ad3f4ecd1 100644 --- a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx @@ -1,8 +1,10 @@ import { Card } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import * as React from 'react'; import AceEditor from 'react-ace'; import { IAssessment, IMCQQuestion } from '../../assessment/assessmentShape'; +import { controlButton } from '../../commons'; import { assignToPath, getValueFromPath } from './'; import TextareaContent from './TextareaContent'; @@ -78,12 +80,37 @@ export class QuestionTemplateTab extends React.Component { {mcqButton} Solution: {this.textareaContent(['questions', questionId, 'solution'], true, [0, 3])} + {controlButton('Add Option', IconNames.CONFIRM, this.addOption)} + {controlButton('Delete Option', IconNames.REMOVE, this.delOption)} ); }; + private addOption = () => { + const assessment = this.props.assessment; + const questionId = this.props.questionId; + const question = assessment!.questions[questionId] as IMCQQuestion; + const choices = question.choices.concat([{ + content: 'A', + hint: null + }]); + question.choices = choices; + assessment!.questions[questionId] = question; + this.props.updateAssessment(assessment); + } + + private delOption = () => { + const assessment = this.props.assessment; + const questionId = this.props.questionId; + const question = assessment!.questions[questionId] as IMCQQuestion; + const choices = question.choices.slice(0, question.choices.length - 1); + question.choices = choices; + assessment!.questions[questionId] = question; + this.props.updateAssessment(assessment); + } + private textareaContent = ( path: Array, isNumber: boolean = false, From 19044bbbe13d8b3f6623592e1e006edf7575e5c2 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 14:08:58 +0800 Subject: [PATCH 06/26] symbols aligned --- .../incubator/editingWorkspaceSideContent/DeploymentTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index 26f99f96de..ec070c99c3 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -61,7 +61,9 @@ export class DeploymentTab extends React.Component ( - {this.textareaContent(deploymentPath.concat(['external', 'symbols', i]))} + + {this.textareaContent(deploymentPath.concat(['external', 'symbols', i]))} + {controlButton('Delete', IconNames.MINUS, this.handleSymbolDelete(i))} )); From 0cf76f4f96bca1389dc736099b57d1b22e113325 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Sun, 24 Mar 2019 15:44:31 +0800 Subject: [PATCH 07/26] increase abstraction removed numberRange from textArea Reduced save frequency of solutionTemplate --- .../GradingTab.tsx | 10 +- .../QuestionTemplateTab.tsx | 105 +++++++++++++----- .../TextareaContent.tsx | 8 +- .../editingWorkspaceSideContent/index.tsx | 14 +++ 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/GradingTab.tsx b/src/components/incubator/editingWorkspaceSideContent/GradingTab.tsx index 04d1ad0140..9f445ef945 100644 --- a/src/components/incubator/editingWorkspaceSideContent/GradingTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/GradingTab.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { IAssessment } from '../../assessment/assessmentShape'; +import { limitNumberRange } from './'; import TextareaContent from './TextareaContent'; interface IProps { @@ -18,12 +19,13 @@ export class GradingTab extends React.Component { return this.gradingTab(); } - private textareaContent = (path: Array, isNumber: boolean = false) => { + private textareaContent = (path: Array) => { return ( ); @@ -32,10 +34,10 @@ export class GradingTab extends React.Component { private gradingTab = () => (
Max Grade: - {this.textareaContent(this.props.path.concat(['maxGrade']), true)} + {this.textareaContent(this.props.path.concat(['maxGrade']))}
Max Xp: - {this.textareaContent(this.props.path.concat(['maxXp']), true)} + {this.textareaContent(this.props.path.concat(['maxXp']))}
); } diff --git a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx index cfe5a3b9d8..cb3bae3f11 100644 --- a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import AceEditor from 'react-ace'; import { IAssessment, IMCQQuestion } from '../../assessment/assessmentShape'; -import { assignToPath, getValueFromPath } from './'; +import { assignToPath, getValueFromPath, limitNumberRange } from './'; import TextareaContent from './TextareaContent'; interface IProps { @@ -12,9 +12,18 @@ interface IProps { updateAssessment: (assessment: IAssessment) => void; } -export class QuestionTemplateTab extends React.Component { +interface IState { + templateValue: string; + templateFocused: boolean; +} + +export class QuestionTemplateTab extends React.Component { public constructor(props: IProps) { super(props); + this.state = { + templateValue: '', + templateFocused: false + }; } public render() { @@ -34,30 +43,60 @@ export class QuestionTemplateTab extends React.Component { const path = ['questions', this.props.questionId, 'answer']; const handleTemplateChange = (newCode: string) => { - const assessmentVal = this.props.assessment; - assignToPath(path, newCode, assessmentVal); - this.props.updateAssessment(assessmentVal); + this.setState({ + templateValue: newCode + }); }; + const value = this.state.templateFocused + ? this.state.templateValue + : getValueFromPath(path, this.props.assessment); + const display = ( - +
+ +
); return display; }; + private focusEditor = (path: Array) => (e: any): void => { + if (!this.state.templateFocused) { + this.setState({ + templateValue: getValueFromPath(path, this.props.assessment), + templateFocused: true + }); + } + }; + + private unFocusEditor = (path: Array) => (e: any): void => { + if (this.state.templateFocused) { + const value = getValueFromPath(path, this.props.assessment); + if (value !== this.state.templateValue) { + const assessmentVal = this.props.assessment; + assignToPath(path, this.state.templateValue, assessmentVal); + this.props.updateAssessment(assessmentVal); + } + this.setState({ + templateValue: '', + templateFocused: false + }); + } + }; + private mcqTab = () => { const questionId = this.props.questionId; const question = this.props.assessment!.questions[questionId] as IMCQQuestion; @@ -87,17 +126,27 @@ export class QuestionTemplateTab extends React.Component { private textareaContent = ( path: Array, isNumber: boolean = false, - numberRange: number[] = [0] + range: number[] = [0] ) => { - return ( - - ); + if (isNumber) { + return ( + + ); + } else { + return ( + + ); + } }; } diff --git a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx index 613b477cb8..14086f182e 100644 --- a/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/TextareaContent.tsx @@ -8,7 +8,6 @@ import { assignToPath, getValueFromPath } from './'; interface IProps { assessment: IAssessment; isNumber?: boolean; - numberRange?: number[]; path: Array; useRawValue?: boolean; processResults?: (newVal: string | number) => string | number; @@ -54,12 +53,9 @@ export class TextareaContent extends React.Component { private saveEditAssessment = (e: any) => { let fieldValue: number | string; if (this.state.isNumber) { - const range = this.props.numberRange || [0]; fieldValue = parseInt(this.state.fieldValue, 10); - if (isNaN(fieldValue) || fieldValue < range[0]) { - fieldValue = range[0]; - } else if (range.length > 1 && fieldValue > range[1]) { - fieldValue = range[1]; + if (isNaN(fieldValue)) { + fieldValue = getValueFromPath(this.props.path, this.props.assessment); } } else { fieldValue = this.state.fieldValue; diff --git a/src/components/incubator/editingWorkspaceSideContent/index.tsx b/src/components/incubator/editingWorkspaceSideContent/index.tsx index d89c4bbc5f..6d73029c78 100644 --- a/src/components/incubator/editingWorkspaceSideContent/index.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/index.tsx @@ -20,3 +20,17 @@ export const assignToPath = (path: Array, value: any, obj: any) } obj[path[i]] = value; }; + +export const limitNumberRange = (min: number | null = 0, max: number | null = null) => ( + value: number +): number => { + let result; + if (min !== null && value < min) { + result = min; + } else if (max !== null && value > max) { + result = max; + } else { + result = value; + } + return result; +}; From 01ffb72d612321ebaf61d1dea2af27cd535116fd Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Sun, 24 Mar 2019 16:18:49 +0800 Subject: [PATCH 08/26] manage questions give warning --- src/components/incubator/EditingWorkspace.tsx | 1 + .../ManageQuestionTab.tsx | 77 +++++++++++++++++-- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 1e790de1d9..8b59643872 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -393,6 +393,7 @@ class AssessmentWorkspace extends React.Component diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index 15cf713006..f374749540 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -1,23 +1,40 @@ +import { ButtonGroup, Classes, Dialog, Intent } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import * as React from 'react'; import { IAssessment } from '../../assessment/assessmentShape'; import { controlButton } from '../../commons'; +import Markdown from '../../commons/Markdown'; import { mcqTemplate, programmingTemplate } from '../../incubator/assessmentTemplates'; interface IProps { assessment: IAssessment; + hasUnsavedChanges: boolean; questionId: number; updateAssessment: (assessment: IAssessment) => void; } -export class ManageQuestionTab extends React.Component { +interface IState { + showSaveOverlay: boolean; + modifyAssessment: () => void; +} + +export class ManageQuestionTab extends React.Component { public constructor(props: IProps) { super(props); + this.state = { + showSaveOverlay: false, + modifyAssessment: () => {} + }; } public render() { - return this.manageQuestionTab(); + return ( +
+ {this.confirmSaveOverlay()} + {this.manageQuestionTab()} +
+ ); } private manageQuestionTab = () => { @@ -26,10 +43,14 @@ export class ManageQuestionTab extends React.Component { {controlButton( 'Make Programming Question', IconNames.FONT, - this.makeQuestion(programmingTemplate) + this.confirmSave(this.makeQuestion(programmingTemplate)) + )} + {controlButton( + 'Make MCQ Question', + IconNames.CONFIRM, + this.confirmSave(this.makeQuestion(mcqTemplate)) )} - {controlButton('Make MCQ Question', IconNames.CONFIRM, this.makeQuestion(mcqTemplate))} - {controlButton('Delete Question', IconNames.REMOVE, this.deleteQn)} + {controlButton('Delete Question', IconNames.REMOVE, this.confirmSave(this.deleteQn))} ); }; @@ -56,6 +77,52 @@ export class ManageQuestionTab extends React.Component { assessment.questions = questions; this.props.updateAssessment(assessment); }; + + private confirmSave = (modifyAssessment: () => void) => () => { + if (this.props.hasUnsavedChanges) { + this.setState({ + showSaveOverlay: true, + modifyAssessment + }); + } else { + modifyAssessment(); + } + }; + + /** + * Asks to save work. + */ + private confirmSaveOverlay = () => ( + +
+ +
+
+ + {controlButton('Cancel', null, () => this.setState({ showSaveOverlay: false }), { + minimal: false + })} + {controlButton( + 'Confirm', + null, + () => { + this.state.modifyAssessment(); + this.setState({ + showSaveOverlay: false + }); + }, + { minimal: false, intent: Intent.DANGER } + )} + +
+
+ ); } export default ManageQuestionTab; From 113829440245c64732d32fa37e9689ad8e8042c4 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Sun, 24 Mar 2019 16:28:37 +0800 Subject: [PATCH 09/26] UI change for deployment tab --- .../DeploymentTab.tsx | 74 ++++++++++++++----- .../QuestionTemplateTab.tsx | 16 ++-- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index ec070c99c3..7ed4aeff37 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -7,6 +7,7 @@ import { sourceChapters } from '../../../reducers/states'; import { ExternalLibraryName, IAssessment, Library } from '../../assessment/assessmentShape'; import { controlButton } from '../../commons'; +import SideContent from '../../workspace/side-content'; import { emptyLibrary } from '../assessmentTemplates'; import { assignToPath, getValueFromPath } from './'; import TextareaContent from './TextareaContent'; @@ -30,10 +31,14 @@ interface IExternal { symbols: string[]; } -export class DeploymentTab extends React.Component { +export class DeploymentTab extends React.Component< + IProps, + { activeTab: number; deploymentEnabled: boolean } +> { public constructor(props: IProps) { super(props); this.state = { + activeTab: 0, deploymentEnabled: false }; } @@ -58,10 +63,10 @@ export class DeploymentTab extends React.Component { const deploymentPath = this.props.pathToLibrary; const deployment = getValueFromPath(deploymentPath, this.props.assessment) as Library; - const deploymentDisp = this.props.isGlobalDeployment ? 'Global Deployment' : 'Local Deployment'; + // const deploymentDisp = this.props.isGlobalDeployment ? 'Global Deployment' : 'Local Deployment'; const symbols = deployment.external.symbols.map((symbol, i) => ( - + {this.textareaContent(deploymentPath.concat(['external', 'symbols', i]))} {controlButton('Delete', IconNames.MINUS, this.handleSymbolDelete(i))} @@ -70,13 +75,13 @@ export class DeploymentTab extends React.Component ( - -
+ +
{this.textareaContent(deploymentPath.concat(['globals', i, 0]))}
- -
+ +
{this.globalValueTextareaContent(i)}
@@ -90,16 +95,8 @@ export class DeploymentTab extends React.Component - {deploymentDisp} {resetLibrary} -
-
- Interpreter: -
- {chapterSelect(deployment.chapter, this.handleChapterSelect)} -
-
+ const symbolsFragment = ( + External Library:
{externalSelect(deployment.external.name, this.handleExternalSelect!)} @@ -109,16 +106,55 @@ export class DeploymentTab extends React.Component {symbols}
{controlButton('New Symbol', IconNames.PLUS, this.handleNewSymbol)} -
-
+
+ ); + + const globalsFragment = ( +
Globals:

{globals}
{controlButton('New Global', IconNames.PLUS, this.handleNewGlobal)} +
+ ); + + const tabs = [ + { + label: `Library`, + icon: IconNames.BOOK, + body: symbolsFragment + }, + { + label: `Globals`, + icon: IconNames.GLOBE, + body: globalsFragment + } + ]; + + return ( +
+ {/* {deploymentDisp} +
*/} + {resetLibrary} +
+ Interpreter: +
+ {chapterSelect(deployment.chapter, this.handleChapterSelect)} +
); }; + private handleChangeActiveTab = (tab: number) => { + this.setState({ + activeTab: tab + }); + }; + private textareaContent = (path: Array) => { return ( { const assessment = this.props.assessment; const questionId = this.props.questionId; const question = assessment!.questions[questionId] as IMCQQuestion; - const choices = question.choices.concat([{ - content: 'A', - hint: null - }]); + const choices = question.choices.concat([ + { + content: 'A', + hint: null + } + ]); question.choices = choices; assessment!.questions[questionId] = question; this.props.updateAssessment(assessment); - } - + }; + private delOption = () => { const assessment = this.props.assessment; const questionId = this.props.questionId; @@ -109,7 +111,7 @@ export class QuestionTemplateTab extends React.Component { question.choices = choices; assessment!.questions[questionId] = question; this.props.updateAssessment(assessment); - } + }; private textareaContent = ( path: Array, From fb1353d76d9a75fa9a056c3b96ea8f0a301a09a4 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Sun, 24 Mar 2019 18:39:41 +0800 Subject: [PATCH 10/26] fixed maxGrade and maxXP calculation --- src/components/incubator/EditingWorkspace.tsx | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 8b59643872..e44e58e35f 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -114,8 +114,8 @@ class AssessmentWorkspace extends React.Component - {this.resetOverlay(questionId)} + {this.resetOverlay()}
); @@ -194,7 +194,7 @@ class AssessmentWorkspace extends React.Component ( + private resetOverlay = () => ( { const assessment = retrieveLocalAssessment()!; - const question = assessment.questions[questionId] as IQuestion; this.handleRefreshLibrary(); this.setState({ assessment, hasUnsavedChanges: false, showResetOverlay: false, - originalMaxGrade: question.maxGrade, - originalMaxXp: question.maxXp + originalMaxGrade: this.getMaxMarks('maxGrade'), + originalMaxXp: this.getMaxMarks('maxXp') }); }, { minimal: false, intent: Intent.DANGER } @@ -269,9 +268,7 @@ class AssessmentWorkspace extends React.Component { - const questionId = this.formatedQuestionId(); - const assessment = this.state.assessment!; - const curGrade = assessment.questions[questionId].maxGrade; + const curGrade = this.getMaxMarks('maxGrade'); const changeGrade = curGrade - this.state.originalMaxGrade; - const curXp = assessment.questions[questionId].maxXp; + const curXp = this.getMaxMarks('maxXp'); const changeXp = curXp - this.state.originalMaxXp; if (changeGrade !== 0 || changeXp !== 0) { const overview = this.props.assessmentOverview; if (changeGrade !== 0) { - overview.maxGrade += changeGrade; + overview.maxGrade = curGrade; } if (changeXp !== 0) { - overview.maxXp += changeXp; + overview.maxXp = curXp; } this.setState({ originalMaxGrade: curGrade, @@ -327,6 +322,14 @@ class AssessmentWorkspace extends React.Component { + let result = 0; + const questions = this.state.assessment!.questions; + for (const question of questions) { + result += question[field]; + } + return result as number; + } private updateEditAssessmentState = (assessmentVal: IAssessment) => { this.setState({ assessment: assessmentVal, @@ -491,6 +494,7 @@ class AssessmentWorkspace extends React.Component (seen.hasOwnProperty(item) ? false : (seen[item] = true))); From 6a173f234fe7e1f98bf7f33858b45da58b91b237 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 09:15:44 +0800 Subject: [PATCH 11/26] added local/global switch --- src/components/incubator/EditingWorkspace.tsx | 210 +++++++++--------- src/components/workspace/ControlBar.tsx | 26 ++- 2 files changed, 133 insertions(+), 103 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index e44e58e35f..8ba665ca69 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -81,6 +81,7 @@ export type DispatchProps = { interface IState { assessment: IAssessment | null; activeTab: number; + editingMode: string; hasUnsavedChanges: boolean; showResetOverlay: boolean; originalMaxGrade: number; @@ -93,6 +94,7 @@ class AssessmentWorkspace extends React.Component { this.setState({ assessment: assessmentVal, @@ -350,108 +352,118 @@ class AssessmentWorkspace extends React.Component { + const toggle = this.state.editingMode === 'question' ? 'global' : 'question'; + this.setState({ + editingMode: toggle + }); + }; + /** Pre-condition: IAssessment has been loaded */ private sideContentProps: (p: AssessmentWorkspaceProps, q: number) => SideContentProps = ( props: AssessmentWorkspaceProps, questionId: number ) => { const assessment = this.state.assessment!; - const tabs = [ - { - label: `Task ${questionId + 1}`, - icon: IconNames.NINJA, - body: ( - - ) - }, - { - label: `${assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, - body: ( - - ) - }, - { - label: `Question Template`, - icon: IconNames.DOCUMENT, - body: ( - - ) - }, - { - label: `Manage Question`, - icon: IconNames.WRENCH, - body: ( - - ) - }, - { - label: `Manage Global Deployment`, - icon: IconNames.GLOBE, - body: ( - - ) - }, - { - label: `Manage Local Deployment`, - icon: IconNames.HOME, - body: ( - - ) + let tabs; + if (this.state.editingMode === 'question') { + tabs = [ + { + label: `Task ${questionId + 1}`, + icon: IconNames.NINJA, + body: ( + + ) + }, + { + label: `Question Template`, + icon: IconNames.DOCUMENT, + body: ( + + ) + }, + { + label: `Manage Local Deployment`, + icon: IconNames.HOME, + body: ( + + ) + }, + { + label: `Grading`, + icon: IconNames.TICK, + body: ( + + ) + } + ]; + const functionsAttached = assessment!.questions[questionId].library.external.symbols; + if (functionsAttached.includes('get_matrix')) { + tabs.push({ + label: `Tone Matrix`, + icon: IconNames.GRID_VIEW, + body: + }); } - ]; - const isGraded = assessment!.questions[questionId].grader !== null; - if (isGraded) { - tabs.push({ - label: `Grading`, - icon: IconNames.TICK, - body: ( - - ) - }); + } else { + tabs = [ + { + label: `${assessment!.category} Briefing`, + icon: IconNames.BRIEFCASE, + body: ( + + ) + }, + { + label: `Manage Question`, + icon: IconNames.WRENCH, + body: ( + + ) + }, + { + label: `Manage Global Deployment`, + icon: IconNames.GLOBE, + body: ( + + ) + } + ]; } - const functionsAttached = assessment!.questions[questionId].library.external.symbols; - if (functionsAttached.includes('get_matrix')) { - tabs.push({ - label: `Tone Matrix`, - icon: IconNames.GRID_VIEW, - body: - }); - } return { activeTab: this.state.activeTab, handleChangeActiveTab: this.handleChangeActiveTab, @@ -469,7 +481,6 @@ class AssessmentWorkspace extends React.Component (seen.hasOwnProperty(item) ? false : (seen[item] = true))); diff --git a/src/components/workspace/ControlBar.tsx b/src/components/workspace/ControlBar.tsx index 0d2de4ee04..536a641991 100644 --- a/src/components/workspace/ControlBar.tsx +++ b/src/components/workspace/ControlBar.tsx @@ -33,11 +33,13 @@ export type ControlBarProps = { hasUnsavedChanges?: boolean; isEditorAutorun?: boolean; isRunning: boolean; + editingMode?: string; onClickNext?(): any; onClickPrevious?(): any; onClickReturn?(): any; onClickSave?(): any; onClickReset?(): any; + toggleEditMode?(): void; }; interface IChapter { @@ -138,9 +140,10 @@ class ControlBar extends React.PureComponent { const stopAutorunButton = this.props.hasEditorAutorunButton ? controlButton('Autorun', IconNames.STOP, this.props.handleToggleEditorAutorun) : undefined; - const resetButton = this.props.hasSaveButton - ? controlButton('Reset', IconNames.REPEAT, this.props.onClickReset) - : undefined; + const resetButton = + this.props.onClickReset !== null + ? controlButton('Reset', IconNames.REPEAT, this.props.onClickReset) + : undefined; return (
{this.props.isEditorAutorun ? undefined : this.props.isRunning ? stopButton : runButton} @@ -182,15 +185,30 @@ class ControlBar extends React.PureComponent { } private replControl() { + const toggleEditModeButton = + this.props.toggleEditMode !== null ? ( + + {controlButton('Switch editing mode', IconNames.REFRESH, this.props.toggleEditMode)} + + ) : ( + undefined + ); const evalButton = ( {controlButton('Eval', IconNames.CODE, this.props.handleReplEval)} ); const clearButton = controlButton('Clear', IconNames.REMOVE, this.props.handleReplOutputClear); + return (
- {this.props.isRunning ? null : evalButton} {clearButton} + {this.props.isRunning ? null : evalButton} {clearButton} {toggleEditModeButton}
); } From a7ee16c6d46b72074ca07590fa8fef3bab1225c4 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 09:22:06 +0800 Subject: [PATCH 12/26] adjust mcq options --- .../editingWorkspaceSideContent/QuestionTemplateTab.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx index 5dbef15fa7..10ae2db712 100644 --- a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx @@ -111,16 +111,18 @@ export class QuestionTemplateTab extends React.Component { {this.textareaContent(['questions', questionId, 'choices', i, 'hint'])}
)); - + const deleteButton = controlButton('Delete Option', IconNames.REMOVE, this.delOption); + return (
{mcqButton} Solution: - {this.textareaContent(['questions', questionId, 'solution'], true, [0, 3])} + {this.textareaContent(['questions', questionId, 'solution'], true, [0, question.choices.length])} +
{controlButton('Add Option', IconNames.CONFIRM, this.addOption)} - {controlButton('Delete Option', IconNames.REMOVE, this.delOption)} + {(question.choices.length > 0) ? deleteButton : undefined}
From 60a7f646663e0a63bc5738ad664bbf39b1fe91e1 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 10:23:50 +0800 Subject: [PATCH 13/26] split question template tab added suggested answer editing --- src/components/incubator/EditingWorkspace.tsx | 33 ++-- ...lateTab.tsx => MCQQuestionTemplateTab.tsx} | 96 ++---------- .../ProgrammingQuestionTemplateTab.tsx | 147 ++++++++++++++++++ .../editingWorkspaceSideContent/index.tsx | 12 +- 4 files changed, 190 insertions(+), 98 deletions(-) rename src/components/incubator/editingWorkspaceSideContent/{QuestionTemplateTab.tsx => MCQQuestionTemplateTab.tsx} (54%) create mode 100644 src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 8ba665ca69..50ff35c09c 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -29,7 +29,8 @@ import { DeploymentTab, GradingTab, ManageQuestionTab, - QuestionTemplateTab, + MCQQuestionTemplateTab, + ProgrammingQuestionTemplateTab, TextareaContentTab } from './editingWorkspaceSideContent'; @@ -269,9 +270,6 @@ class AssessmentWorkspace extends React.Component { const toggle = this.state.editingMode === 'question' ? 'global' : 'question'; this.setState({ + activeTab: 0, editingMode: toggle }); }; @@ -367,6 +366,24 @@ class AssessmentWorkspace extends React.Component + ) : ( + + ); + tabs = [ { label: `Task ${questionId + 1}`, @@ -382,13 +399,7 @@ class AssessmentWorkspace extends React.Component - ) + body: questionTemplateTab }, { label: `Manage Local Deployment`, diff --git a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx similarity index 54% rename from src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx rename to src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx index 10ae2db712..f1c09d7e45 100644 --- a/src/components/incubator/editingWorkspaceSideContent/QuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/MCQQuestionTemplateTab.tsx @@ -1,11 +1,10 @@ import { Card } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import * as React from 'react'; -import AceEditor from 'react-ace'; import { IAssessment, IMCQQuestion } from '../../assessment/assessmentShape'; import { controlButton } from '../../commons'; -import { assignToPath, getValueFromPath, limitNumberRange } from './'; +import { limitNumberRange } from './'; import TextareaContent from './TextareaContent'; interface IProps { @@ -14,91 +13,15 @@ interface IProps { updateAssessment: (assessment: IAssessment) => void; } -interface IState { - templateValue: string; - templateFocused: boolean; -} - -export class QuestionTemplateTab extends React.Component { +export class MCQQuestionTemplateTab extends React.Component { public constructor(props: IProps) { super(props); - this.state = { - templateValue: '', - templateFocused: false - }; } public render() { - return this.questionTemplateTab(); + return this.mcqTab(); } - private questionTemplateTab = () => { - // tslint:disable-next-line:no-console - // console.dir(this.props.assessment) - const type = this.props.assessment!.questions[this.props.questionId].type; - const display = type === 'mcq' ? this.mcqTab() : this.programmingTab(); - - return display; - }; - - private programmingTab = () => { - const path = ['questions', this.props.questionId, 'answer']; - - const handleTemplateChange = (newCode: string) => { - this.setState({ - templateValue: newCode - }); - }; - - const value = this.state.templateFocused - ? this.state.templateValue - : getValueFromPath(path, this.props.assessment); - - const display = ( -
- -
- ); - - return display; - }; - - private focusEditor = (path: Array) => (e: any): void => { - if (!this.state.templateFocused) { - this.setState({ - templateValue: getValueFromPath(path, this.props.assessment), - templateFocused: true - }); - } - }; - - private unFocusEditor = (path: Array) => (e: any): void => { - if (this.state.templateFocused) { - const value = getValueFromPath(path, this.props.assessment); - if (value !== this.state.templateValue) { - const assessmentVal = this.props.assessment; - assignToPath(path, this.state.templateValue, assessmentVal); - this.props.updateAssessment(assessmentVal); - } - this.setState({ - templateValue: '', - templateFocused: false - }); - } - }; - private mcqTab = () => { const questionId = this.props.questionId; const question = this.props.assessment!.questions[questionId] as IMCQQuestion; @@ -112,17 +35,20 @@ export class QuestionTemplateTab extends React.Component {
)); const deleteButton = controlButton('Delete Option', IconNames.REMOVE, this.delOption); - + return (
{mcqButton} Solution: - {this.textareaContent(['questions', questionId, 'solution'], true, [0, question.choices.length])} -
+ {this.textareaContent(['questions', questionId, 'solution'], true, [ + 0, + question.choices.length + ])} +
{controlButton('Add Option', IconNames.CONFIRM, this.addOption)} - {(question.choices.length > 0) ? deleteButton : undefined} + {question.choices.length > 0 ? deleteButton : undefined}
@@ -181,4 +107,4 @@ export class QuestionTemplateTab extends React.Component { }; } -export default QuestionTemplateTab; +export default MCQQuestionTemplateTab; diff --git a/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx new file mode 100644 index 0000000000..33c8e282cd --- /dev/null +++ b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx @@ -0,0 +1,147 @@ +import { Switch } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; +import AceEditor from 'react-ace'; + +import { IAssessment } from '../../assessment/assessmentShape'; +import { controlButton } from '../../commons'; +import { assignToPath, getValueFromPath } from './'; + +interface IProps { + assessment: IAssessment; + editorValue: string | null; + questionId: number; + updateAssessment: (assessment: IAssessment) => void; + handleEditorValueChange: (val: string) => void; +} + +interface IState { + templateValue: string; + templateFocused: boolean; + isSuggestedAnswer: boolean; +} + +export class ProgrammingQuestionTemplateTab extends React.Component { + public constructor(props: IProps) { + super(props); + this.state = { + templateValue: '', + templateFocused: false, + isSuggestedAnswer: false + }; + } + + public render() { + return this.programmingTab(); + } + + private programmingTab = () => { + const qnPath = ['questions', this.props.questionId]; + const templateSwitch = ( + + ); + const path = this.state.isSuggestedAnswer + ? qnPath.concat(['solutionTemplate']) + : qnPath.concat(['answer']); + + const copyFromEditorButton = controlButton( + 'Copy from Editor', + IconNames.IMPORT, + this.handleCopyFromEditor(path) + ); + + const copyToEditorButton = controlButton( + 'Copy to Editor', + IconNames.EXPORT, + this.handleCopyToEditor(path) + ); + + const handleTemplateChange = (newCode: string) => { + this.setState({ + templateValue: newCode + }); + }; + + const value = this.state.templateFocused + ? this.state.templateValue + : getValueFromPath(path, this.props.assessment); + + const editor = ( +
+ +
+ ); + + return ( +
+ {templateSwitch} +
+ {copyFromEditorButton} + {copyToEditorButton} +
+
+ {editor} + } +
+ ); + }; + + private focusEditor = (path: Array) => (e: any): void => { + if (!this.state.templateFocused) { + this.setState({ + templateValue: getValueFromPath(path, this.props.assessment), + templateFocused: true + }); + } + }; + + private unFocusEditor = (path: Array) => (e: any): void => { + if (this.state.templateFocused) { + const value = getValueFromPath(path, this.props.assessment); + if (value !== this.state.templateValue) { + const assessmentVal = this.props.assessment; + assignToPath(path, this.state.templateValue, assessmentVal); + this.props.updateAssessment(assessmentVal); + } + this.setState({ + templateValue: '', + templateFocused: false + }); + } + }; + + private toggleTemplateMode = () => { + this.setState({ + isSuggestedAnswer: !this.state.isSuggestedAnswer + }); + }; + + private handleCopyFromEditor = (path: Array) => (): void => { + const assessment = this.props.assessment; + assignToPath(path, this.props.editorValue, assessment); + this.props.updateAssessment(assessment); + }; + + private handleCopyToEditor = (path: Array) => (): void => { + const value = getValueFromPath(path, this.props.assessment); + this.props.handleEditorValueChange(value); + }; +} + +export default ProgrammingQuestionTemplateTab; diff --git a/src/components/incubator/editingWorkspaceSideContent/index.tsx b/src/components/incubator/editingWorkspaceSideContent/index.tsx index 6d73029c78..7aef06beae 100644 --- a/src/components/incubator/editingWorkspaceSideContent/index.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/index.tsx @@ -1,10 +1,18 @@ import DeploymentTab from './DeploymentTab'; import GradingTab from './GradingTab'; import ManageQuestionTab from './ManageQuestionTab'; -import QuestionTemplateTab from './QuestionTemplateTab'; +import MCQQuestionTemplateTab from './MCQQuestionTemplateTab'; +import ProgrammingQuestionTemplateTab from './ProgrammingQuestionTemplateTab'; import TextareaContentTab from './TextareaContent'; -export { DeploymentTab, GradingTab, ManageQuestionTab, QuestionTemplateTab, TextareaContentTab }; +export { + DeploymentTab, + GradingTab, + ManageQuestionTab, + MCQQuestionTemplateTab, + ProgrammingQuestionTemplateTab, + TextareaContentTab +}; export const getValueFromPath = (path: Array, obj: any): any => { for (const next of path) { From c1cb2a79e1bb393498bc13e85bb2cda6d923b7ea Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 10:28:48 +0800 Subject: [PATCH 14/26] swap answer and solutionTemplate internal change --- src/components/incubator/EditingWorkspace.tsx | 4 ++-- .../ProgrammingQuestionTemplateTab.tsx | 4 ++-- src/utils/xmlParser.ts | 7 +++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 50ff35c09c..6b5a6a551d 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -113,8 +113,8 @@ class AssessmentWorkspace extends React.Component ); const path = this.state.isSuggestedAnswer - ? qnPath.concat(['solutionTemplate']) - : qnPath.concat(['answer']); + ? qnPath.concat(['answer']) + : qnPath.concat(['solutionTemplate']); const copyFromEditorButton = controlButton( 'Copy from Editor', diff --git a/src/utils/xmlParser.ts b/src/utils/xmlParser.ts index 54765a77ce..14809b0e3f 100644 --- a/src/utils/xmlParser.ts +++ b/src/utils/xmlParser.ts @@ -208,8 +208,8 @@ const makeProgramming = ( ): IProgrammingQuestion => { const result: IProgrammingQuestion = { ...question, - answer: problem.SNIPPET[0].TEMPLATE[0] as string, - solutionTemplate: problem.SNIPPET[0].SOLUTION[0] as string, + solutionTemplate: problem.SNIPPET[0].TEMPLATE[0] as string, + answer: problem.SNIPPET[0].SOLUTION[0] as string, type: 'programming' }; if (problem.SNIPPET[0].GRADER) { @@ -336,13 +336,12 @@ export const assessmentToXml = ( } if (question.type === 'programming') { - problem.SNIPPET.SOLUTION = question.solutionTemplate; if (question.graderTemplate) { /* tslint:disable:no-string-literal */ problem.SNIPPET['GRADER'] = question.graderTemplate; } /* tslint:disable:no-string-literal */ - problem.SNIPPET['TEMPLATE'] = question.answer; + problem.SNIPPET['TEMPLATE'] = question.solutionTemplate; } if (question.type === 'mcq') { From 7460f3c25b37042bfe6e5c241aa7fdf463c4a933 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 10:43:02 +0800 Subject: [PATCH 15/26] add reading --- src/components/assessment/assessmentShape.ts | 1 + src/components/incubator/assessmentTemplates.ts | 5 +++-- src/utils/xmlParser.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index 6bd85067f7..f03483655c 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -16,6 +16,7 @@ export interface IAssessmentOverview { maxXp: number; openAt: string; title: string; + reading?: string; shortSummary: string; status: AssessmentStatus; story: string | null; diff --git a/src/components/incubator/assessmentTemplates.ts b/src/components/incubator/assessmentTemplates.ts index 10a77e5e8c..bd1f1a213a 100644 --- a/src/components/incubator/assessmentTemplates.ts +++ b/src/components/incubator/assessmentTemplates.ts @@ -41,6 +41,7 @@ export const overviewTemplate = (): IAssessmentOverview => { maxXp: 0, openAt: '2000-01-01T00:00+08', title: 'Insert title here', + reading: '', shortSummary: 'Insert short summary here', status: AssessmentStatuses.not_attempted, story: 'mission', @@ -51,13 +52,13 @@ export const overviewTemplate = (): IAssessmentOverview => { export const programmingTemplate = (): IProgrammingQuestion => { return { - answer: '//1st question mock solution template', + answer: '\/\/ [Marking Scheme]\n\/\/ 1 mark for correct answer', comment: '`Great Job` **young padawan**', content: 'Enter content here', id: 0, library: emptyLibrary(), graderLibrary: emptyLibrary(), - solutionTemplate: '//This is a mock solution template', + solutionTemplate: '\/\/This is a mock solution template', type: 'programming', grader: { name: 'avenger', diff --git a/src/utils/xmlParser.ts b/src/utils/xmlParser.ts index 14809b0e3f..d373ad1ed4 100644 --- a/src/utils/xmlParser.ts +++ b/src/utils/xmlParser.ts @@ -84,6 +84,7 @@ const makeAssessmentOverview = ( maxXp: maxXpVal, openAt: rawOverview.startdate, title: rawOverview.title, + reading: task.READING !== null ? task.READING[0] : '', shortSummary: task.WEBSUMMARY ? task.WEBSUMMARY[0] : '', status: AssessmentStatuses.attempting, story: rawOverview.story, @@ -297,6 +298,10 @@ export const assessmentToXml = ( }; task.$ = rawOverview; + if (overview.reading && overview.reading !== '') { + task.READING = overview.reading; + } + task.WEBSUMMARY = overview.shortSummary; task.TEXT = assessment.longSummary; task.PROBLEMS = { PROBLEM: [] }; From 6ffe9cbbbab522422e635f7397da87bd4b96f5f3 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 12:51:11 +0800 Subject: [PATCH 16/26] add grading deployment --- src/components/incubator/EditingWorkspace.tsx | 49 ++++++++++++++--- .../incubator/assessmentTemplates.ts | 4 +- .../DeploymentTab.tsx | 52 ++++++++++++------- .../ProgrammingQuestionTemplateTab.tsx | 1 - 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 6b5a6a551d..73a67d1649 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -255,7 +255,7 @@ class AssessmentWorkspace extends React.Component { + private handleRefreshLibrary = (library: Library | undefined = undefined) => { const question = this.state.assessment!.questions[this.formatedQuestionId()]; - let library = - question.library.chapter === -1 ? this.state.assessment!.globalDeployment! : question.library; + if (!library) { + library = + question.library.chapter === -1 + ? this.state.assessment!.globalDeployment! + : question.library; + } if (library && library.globals.length > 0) { const globalsVal = library.globals.map((x: any) => x[0]); const symbolsVal = library.external.symbols.concat(globalsVal); @@ -296,7 +300,7 @@ class AssessmentWorkspace extends React.Component + ) + }, + { + label: `Manage Local Grader Deployment`, + icon: IconNames.CONFIRM, + body: ( + ) }, @@ -465,10 +485,25 @@ class AssessmentWorkspace extends React.Component + ) + }, + { + label: `Manage Global Grader Deployment`, + icon: IconNames.CONFIRM, + body: ( + ) } diff --git a/src/components/incubator/assessmentTemplates.ts b/src/components/incubator/assessmentTemplates.ts index bd1f1a213a..e395859e87 100644 --- a/src/components/incubator/assessmentTemplates.ts +++ b/src/components/incubator/assessmentTemplates.ts @@ -52,13 +52,13 @@ export const overviewTemplate = (): IAssessmentOverview => { export const programmingTemplate = (): IProgrammingQuestion => { return { - answer: '\/\/ [Marking Scheme]\n\/\/ 1 mark for correct answer', + answer: '// [Marking Scheme]\n// 1 mark for correct answer', comment: '`Great Job` **young padawan**', content: 'Enter content here', id: 0, library: emptyLibrary(), graderLibrary: emptyLibrary(), - solutionTemplate: '\/\/This is a mock solution template', + solutionTemplate: '//This is a mock solution template', type: 'programming', grader: { name: 'avenger', diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index ee51e6c447..30c5533753 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -14,10 +14,12 @@ import TextareaContent from './TextareaContent'; interface IProps { assessment: IAssessment; + label: string; pathToLibrary: Array; + pathToCopy?: Array; updateAssessment: (assessment: IAssessment) => void; handleRefreshLibrary: (library: Library) => void; - isGlobalDeployment: boolean; + isOptionalDeployment: boolean; } interface IChapter { @@ -31,30 +33,33 @@ interface IExternal { symbols: string[]; } -export class DeploymentTab extends React.Component< - IProps, - { activeTab: number; deploymentEnabled: boolean } -> { +export class DeploymentTab extends React.Component { public constructor(props: IProps) { super(props); this.state = { - activeTab: 0, - deploymentEnabled: false + activeTab: 0 }; } public render() { - if (this.props.isGlobalDeployment) { - return this.deploymentTab(); + if (!this.props.isOptionalDeployment) { + return ( +
+ {this.props.label + ' Deployment'} +
+ {this.deploymentTab()} + } +
+ ); } else { return (
- {this.state.deploymentEnabled ? this.deploymentTab() : null} + {this.isEmptyLibrary() ? null : this.deploymentTab()}
); } @@ -91,7 +96,7 @@ export class DeploymentTab extends React.Component< )); - const resetLibrary = controlButton('Refresh Library', IconNames.REFRESH, () => + const resetLibrary = controlButton('Use this Library', IconNames.REFRESH, () => this.props.handleRefreshLibrary(deployment) ); @@ -237,16 +242,25 @@ export class DeploymentTab extends React.Component< private handleSwitchDeployment = () => { const assessment = this.props.assessment; - if (this.state.deploymentEnabled) { - assignToPath(this.props.pathToLibrary, emptyLibrary(), assessment); + if (this.isEmptyLibrary()) { + let library = getValueFromPath( + this.props.pathToCopy || ['globalDeployment'], + assessment + ) as Library; + if (library.chapter === -1) { + library = assessment.globalDeployment!; + } + library = JSON.parse(JSON.stringify(library)); + assignToPath(this.props.pathToLibrary, library, assessment); } else { - assignToPath(this.props.pathToLibrary.concat(['chapter']), 1, assessment); + assignToPath(this.props.pathToLibrary, emptyLibrary(), assessment); } - this.setState({ - deploymentEnabled: !this.state.deploymentEnabled - }); this.props.updateAssessment(assessment); }; + + private isEmptyLibrary = (path: Array = this.props.pathToLibrary) => { + return getValueFromPath(path.concat(['chapter']), this.props.assessment) === -1; + }; } const removeSpaces = (str: string) => { diff --git a/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx index 8e31363aa4..049c0a773a 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ProgrammingQuestionTemplateTab.tsx @@ -91,7 +91,6 @@ export class ProgrammingQuestionTemplateTab extends React.Component {templateSwitch} -
{copyFromEditorButton} {copyToEditorButton}
From 05dd14addde246f29e043dca38fe60c2a9c3e640 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Mon, 25 Mar 2019 13:41:15 +0800 Subject: [PATCH 17/26] clone question shift question added --- .../DeploymentTab.tsx | 10 ++--- .../ManageQuestionTab.tsx | 40 ++++++++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index ee51e6c447..18d25082f3 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -75,15 +75,13 @@ export class DeploymentTab extends React.Component< const globals = deployment.globals.map((symbol, i) => ( - -
+ +
{this.textareaContent(deploymentPath.concat(['globals', i, 0]))}
- -
- {this.globalValueTextareaContent(i)} -
+ +
{this.globalValueTextareaContent(i)}
{controlButton('Delete', IconNames.MINUS, this.handleGlobalDelete(i))} diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index fb41a04f26..7036d48951 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -40,6 +40,14 @@ export class ManageQuestionTab extends React.Component { private manageQuestionTab = () => { return (
+ {controlButton( + 'Clone Current Question', + IconNames.DOCUMENT, + this.confirmSave( + this.makeQuestion(() => this.props.assessment.questions[this.props.questionId]) + ) + )} +
{controlButton( 'Insert Programming Question', IconNames.FONT, @@ -50,15 +58,43 @@ export class ManageQuestionTab extends React.Component { IconNames.CONFIRM, this.confirmSave(this.makeQuestion(mcqTemplate)) )} +
{controlButton( 'Delete Current Question', IconNames.REMOVE, - this.confirmSave(this.deleteQn) + this.confirmSave(this.deleteQuestion) + )} +
+ {controlButton( + 'Shift Question Left', + IconNames.CARET_LEFT, + this.confirmSave(() => this.shiftQuestion(-1)) + )} + {controlButton( + 'Shift Question Right', + IconNames.CARET_RIGHT, + this.confirmSave(() => this.shiftQuestion(1)) )}
); }; + private shiftQuestion = (dir: number) => { + const assessment = this.props.assessment; + const index = this.props.questionId; + const question = assessment.questions[index]; + let questions = assessment.questions; + questions = questions.slice(0, index).concat(questions.slice(index + 1)); + questions = questions + .slice(0, index + dir) + .concat([question]) + .concat(questions.slice(index + dir)); + assessment.questions = questions; + // tslint:disable-next-line:no-console + console.log(questions); + this.props.updateAssessment(assessment); + }; + private makeQuestion = (template: () => any) => () => { const assessment = this.props.assessment; const index = this.props.questionId; @@ -71,7 +107,7 @@ export class ManageQuestionTab extends React.Component { this.props.updateAssessment(assessment); }; - private deleteQn = () => { + private deleteQuestion = () => { const assessment = this.props.assessment; let questions = assessment.questions; const index = this.props.questionId; From 9dce47befa266c7a3dd3664be571e5b725cd56f8 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Mon, 25 Mar 2019 18:25:30 +0800 Subject: [PATCH 18/26] fixed symbol formating --- src/components/incubator/EditingWorkspace.tsx | 24 ++++--- .../incubator/assessmentTemplates.ts | 1 + .../DeploymentTab.tsx | 29 ++++---- .../ManageQuestionTab.tsx | 67 +++++++++++-------- src/components/incubator/index.tsx | 2 +- 5 files changed, 67 insertions(+), 56 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 73a67d1649..e42a7f0e47 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { InterpreterOutput, IWorkspaceState } from '../../reducers/states'; import { history } from '../../utils/history'; -import { assessmentCategoryLink } from '../../utils/paramParseHelpers'; import { retrieveLocalAssessment, storeLocalAssessment, @@ -55,7 +54,6 @@ export type OwnProps = { questionId: number; assessmentOverview: IAssessmentOverview; updateAssessmentOverview: (overview: IAssessmentOverview) => void; - listingPath?: string; notAttempted: boolean; closeDate: string; }; @@ -110,12 +108,7 @@ class AssessmentWorkspace extends React.Component { + const question: IQuestion = this.state.assessment!.questions[this.formatedQuestionId()]; + const editorValue = + question.type === QuestionTypes.programming + ? ((question as IProgrammingQuestion).solutionTemplate as string) + : '//If you see this, this is a bug. Please report bug.'; + this.props.handleEditorValueChange(editorValue); + }; + private handleSave = () => { this.setState({ hasUnsavedChanges: false @@ -345,7 +347,9 @@ class AssessmentWorkspace extends React.Component { @@ -522,9 +526,7 @@ class AssessmentWorkspace extends React.Component { - const listingPath = - this.props.listingPath || - `/academy/${assessmentCategoryLink(this.state.assessment!.category)}`; + const listingPath = '/incubator'; const assessmentWorkspacePath = listingPath + `/${this.state.assessment!.id.toString()}`; return { handleEditorEval: this.props.handleEditorEval, diff --git a/src/components/incubator/assessmentTemplates.ts b/src/components/incubator/assessmentTemplates.ts index e395859e87..359c5fea2e 100644 --- a/src/components/incubator/assessmentTemplates.ts +++ b/src/components/incubator/assessmentTemplates.ts @@ -97,6 +97,7 @@ export const mcqTemplate = (): IMCQQuestion => { ], id: 2, library: emptyLibrary(), + graderLibrary: emptyLibrary(), type: 'mcq', solution: 0, grader: { diff --git a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx index c8066be0ff..73a9c90ff2 100644 --- a/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/DeploymentTab.tsx @@ -48,7 +48,6 @@ export class DeploymentTab extends React.Component {this.deploymentTab()} - }
); } else { @@ -71,24 +70,20 @@ export class DeploymentTab extends React.Component ( - - {this.textareaContent(deploymentPath.concat(['external', 'symbols', i]))} + {this.textareaContent(deploymentPath.concat(['external', 'symbols', i]))} + + {controlButton('Delete', IconNames.MINUS, this.handleSymbolDelete(i))} - {controlButton('Delete', IconNames.MINUS, this.handleSymbolDelete(i))} )); const globals = deployment.globals.map((symbol, i) => ( - -
- {this.textareaContent(deploymentPath.concat(['globals', i, 0]))} -
+ + {this.textareaContent(deploymentPath.concat(['globals', i, 0]))} - -
{this.globalValueTextareaContent(i)}
- - + {this.globalValueTextareaContent(i)} + {controlButton('Delete', IconNames.MINUS, this.handleGlobalDelete(i))} @@ -107,7 +102,9 @@ export class DeploymentTab extends React.Component
Symbols:

- {symbols}
+ + {symbols} +
{controlButton('New Symbol', IconNames.PLUS, this.handleNewSymbol)} ); @@ -116,7 +113,9 @@ export class DeploymentTab extends React.Component
Globals:

- {globals}
+ + {globals} +
{controlButton('New Global', IconNames.PLUS, this.handleNewGlobal)} ); @@ -189,7 +188,7 @@ export class DeploymentTab extends React.Component { } private manageQuestionTab = () => { + const index = this.props.questionId; return (
{controlButton( 'Clone Current Question', IconNames.DOCUMENT, this.confirmSave( - this.makeQuestion(() => this.props.assessment.questions[this.props.questionId]) + this.makeQuestion(() => + deepCopy(this.props.assessment.questions[this.props.questionId]) + ) ) )}
@@ -65,46 +69,47 @@ export class ManageQuestionTab extends React.Component { this.confirmSave(this.deleteQuestion) )}
- {controlButton( - 'Shift Question Left', - IconNames.CARET_LEFT, - this.confirmSave(() => this.shiftQuestion(-1)) - )} - {controlButton( - 'Shift Question Right', - IconNames.CARET_RIGHT, - this.confirmSave(() => this.shiftQuestion(1)) - )} + {index > 0 + ? controlButton( + 'Shift Question Left', + IconNames.CARET_LEFT, + this.confirmSave(this.shiftQuestion(-1)) + ) + : undefined} + {index < this.props.assessment.questions.length - 1 + ? controlButton( + 'Shift Question Right', + IconNames.CARET_RIGHT, + this.confirmSave(this.shiftQuestion(1)) + ) + : undefined}
); }; - private shiftQuestion = (dir: number) => { + private shiftQuestion = (dir: number) => () => { const assessment = this.props.assessment; const index = this.props.questionId; - const question = assessment.questions[index]; - let questions = assessment.questions; - questions = questions.slice(0, index).concat(questions.slice(index + 1)); - questions = questions - .slice(0, index + dir) - .concat([question]) - .concat(questions.slice(index + dir)); - assessment.questions = questions; - // tslint:disable-next-line:no-console - console.log(questions); - this.props.updateAssessment(assessment); + const newIndex = index + dir; + if (newIndex >= 0 && newIndex < assessment.questions.length) { + const question = assessment.questions[index]; + const questions = assessment.questions; + questions[index] = questions[newIndex]; + questions[newIndex] = question; + assessment.questions = questions; + this.props.updateAssessment(assessment); + history.push('/incubator/-1/' + newIndex.toString()); + } }; private makeQuestion = (template: () => any) => () => { const assessment = this.props.assessment; - const index = this.props.questionId; - let questions = assessment.questions; - questions = questions - .slice(0, index) - .concat([template()]) - .concat(questions.slice(index)); + const index = this.props.questionId + 1; + const questions = assessment.questions; + questions.splice(index, 0, template()); assessment.questions = questions; this.props.updateAssessment(assessment); + history.push('/incubator/-1/' + index.toString()); }; private deleteQuestion = () => { @@ -165,4 +170,8 @@ export class ManageQuestionTab extends React.Component { ); } +const deepCopy = (arr: any) => { + return JSON.parse(JSON.stringify(arr)); +}; + export default ManageQuestionTab; diff --git a/src/components/incubator/index.tsx b/src/components/incubator/index.tsx index 3d2a82ac41..6e90f12c8c 100644 --- a/src/components/incubator/index.tsx +++ b/src/components/incubator/index.tsx @@ -67,7 +67,7 @@ class Assessment extends React.Component { }; return (
- +
); } From de759121102bffd2d58380b1673a1fe805709156 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Tue, 26 Mar 2019 23:00:09 +0800 Subject: [PATCH 19/26] changed manage questions ui --- .../ManageQuestionTab.tsx | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index 18bf036033..5a346c60e3 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Classes, Dialog, Intent } from '@blueprintjs/core'; +import { Button, ButtonGroup, Classes, Dialog, Intent } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import * as React from 'react'; @@ -33,63 +33,74 @@ export class ManageQuestionTab extends React.Component { return (
{this.confirmSaveOverlay()} - {this.manageQuestionTab()} + {this.props.assessment.questions.map((q, index) => ( +
+ Question {index + 1} +
+ + {this.manageQuestionTab(index)} +
+
+ ))}
); } - private manageQuestionTab = () => { - const index = this.props.questionId; + private manageQuestionTab = (index: number) => { return (
{controlButton( - 'Clone Current Question', + `Clone Question ${index + 1}`, IconNames.DOCUMENT, this.confirmSave( this.makeQuestion(() => - deepCopy(this.props.assessment.questions[this.props.questionId]) + deepCopy(this.props.assessment.questions[index]), + index ) ) )} + {controlButton( + `Delete Question ${index + 1}`, + IconNames.REMOVE, + this.confirmSave(this.deleteQuestion(index)) + )}
{controlButton( 'Insert Programming Question', IconNames.FONT, - this.confirmSave(this.makeQuestion(programmingTemplate)) + this.confirmSave(this.makeQuestion(programmingTemplate, index)) )} {controlButton( 'Insert MCQ Question', IconNames.CONFIRM, - this.confirmSave(this.makeQuestion(mcqTemplate)) - )} -
- {controlButton( - 'Delete Current Question', - IconNames.REMOVE, - this.confirmSave(this.deleteQuestion) + this.confirmSave(this.makeQuestion(mcqTemplate, index)) )}
{index > 0 ? controlButton( - 'Shift Question Left', - IconNames.CARET_LEFT, - this.confirmSave(this.shiftQuestion(-1)) + `Shift Question ${index + 1} Up`, + IconNames.CARET_UP, + this.confirmSave(this.shiftQuestion(-1, index)) ) : undefined} {index < this.props.assessment.questions.length - 1 ? controlButton( - 'Shift Question Right', - IconNames.CARET_RIGHT, - this.confirmSave(this.shiftQuestion(1)) + `Shift Question ${index + 1} Down`, + IconNames.CARET_DOWN, + this.confirmSave(this.shiftQuestion(1, index)) ) : undefined}
); }; - private shiftQuestion = (dir: number) => () => { + private shiftQuestion = (dir: number, index: number) => () => { const assessment = this.props.assessment; - const index = this.props.questionId; const newIndex = index + dir; if (newIndex >= 0 && newIndex < assessment.questions.length) { const question = assessment.questions[index]; @@ -102,9 +113,9 @@ export class ManageQuestionTab extends React.Component { } }; - private makeQuestion = (template: () => any) => () => { + private makeQuestion = (template: () => any, index: number) => () => { const assessment = this.props.assessment; - const index = this.props.questionId + 1; + index = index + 1; const questions = assessment.questions; questions.splice(index, 0, template()); assessment.questions = questions; @@ -112,10 +123,9 @@ export class ManageQuestionTab extends React.Component { history.push('/incubator/-1/' + index.toString()); }; - private deleteQuestion = () => { + private deleteQuestion = (index: number) => () => { const assessment = this.props.assessment; let questions = assessment.questions; - const index = this.props.questionId; if (questions.length > 1) { questions = questions.slice(0, index).concat(questions.slice(index + 1)); } From 74dbeda71d336abd3eb56d7d535121cf85ba2945 Mon Sep 17 00:00:00 2001 From: Kai Zhe Date: Tue, 26 Mar 2019 23:01:23 +0800 Subject: [PATCH 20/26] yarn format --- .../ManageQuestionTab.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index 5a346c60e3..1c58fd8c24 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -35,17 +35,16 @@ export class ManageQuestionTab extends React.Component { {this.confirmSaveOverlay()} {this.props.assessment.questions.map((q, index) => (
- Question {index + 1} -
- - {this.manageQuestionTab(index)} -
-
+ {this.manageQuestionTab(index)} +
+ ))} ); @@ -58,10 +57,7 @@ export class ManageQuestionTab extends React.Component { `Clone Question ${index + 1}`, IconNames.DOCUMENT, this.confirmSave( - this.makeQuestion(() => - deepCopy(this.props.assessment.questions[index]), - index - ) + this.makeQuestion(() => deepCopy(this.props.assessment.questions[index]), index) ) )} {controlButton( From ffde4eeae51125f2b3ac4ab1ada6c06c45d852b7 Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Wed, 27 Mar 2019 06:53:30 +0800 Subject: [PATCH 21/26] added more overview options --- src/components/assessment/assessmentShape.ts | 1 + .../incubator/EditingOverviewCard.tsx | 109 ++++++++++++++++-- .../incubator/ImportFromFileComponent.tsx | 8 +- .../DeploymentTab.tsx | 8 +- src/utils/xmlParser.ts | 18 +-- 5 files changed, 115 insertions(+), 29 deletions(-) diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index f03483655c..24598638f2 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -10,6 +10,7 @@ export interface IAssessmentOverview { category: AssessmentCategory; closeAt: string; coverImage: string; + fileName?: string; grade: number; id: number; maxGrade: number; diff --git a/src/components/incubator/EditingOverviewCard.tsx b/src/components/incubator/EditingOverviewCard.tsx index ff20dbe181..23fddef5af 100644 --- a/src/components/incubator/EditingOverviewCard.tsx +++ b/src/components/incubator/EditingOverviewCard.tsx @@ -1,5 +1,6 @@ -import { Button, Card, Elevation, Icon, IconName, Intent, Text } from '@blueprintjs/core'; +import { Button, Card, Classes, Dialog, Elevation, Icon, IconName, Intent, MenuItem, Text } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { ItemRenderer, Select } from '@blueprintjs/select'; import * as React from 'react'; import { NavLink } from 'react-router-dom'; import Textarea from 'react-textarea-autosize'; @@ -9,7 +10,7 @@ import { getPrettyDate } from '../../utils/dateHelpers'; import { assessmentCategoryLink } from '../../utils/paramParseHelpers'; import { exportXml } from '../../utils/xmlParser'; -import { IAssessmentOverview } from '../assessment/assessmentShape'; +import { AssessmentCategories, AssessmentCategory, IAssessmentOverview } from '../assessment/assessmentShape'; import { controlButton } from '../commons'; import Markdown from '../commons/Markdown'; @@ -24,6 +25,7 @@ type Props = { interface IState { editingOverviewField: string; fieldValue: any; + showOptionsOverlay: boolean; } export class EditingOverviewCard extends React.Component { @@ -31,12 +33,13 @@ export class EditingOverviewCard extends React.Component { super(props); this.state = { editingOverviewField: '', - fieldValue: '' + fieldValue: '', + showOptionsOverlay: false }; } public render() { - return
{this.makeEditingOverviewCard(this.props.overview)}
; + return
{this.optionsOverlay()}{this.makeEditingOverviewCard(this.props.overview)}
; } private saveEditOverview = (field: keyof IAssessmentOverview) => (e: any) => { @@ -67,7 +70,13 @@ export class EditingOverviewCard extends React.Component { } }; - private handleExportXml = () => (e: any) => { + private toggleOptionsOverlay = () => { + this.setState({ + showOptionsOverlay: !this.state.showOptionsOverlay + }); + }; + + private handleExportXml = (e: any) => { exportXml(); }; @@ -127,6 +136,7 @@ export class EditingOverviewCard extends React.Component { : `${getPrettyDate(overview.closeAt)}`} + {this.makeOptionsButton()} {makeOverviewCardButton(overview, this.props.listingPath)} @@ -149,22 +159,73 @@ export class EditingOverviewCard extends React.Component { private makeExportButton = (overview: IAssessmentOverview) => ( ); + + private makeOptionsButton = () => + + + private saveCategory = (i: AssessmentCategory, e: any) => { + const overview = { + ...this.props.overview, + category: i + }; + localStorage.setItem('MissionEditingOverviewSA', JSON.stringify(overview)); + this.props.updateEditingOverview(overview); + }; + + private optionsOverlay = () => ( + +
+

Category

+ {categorySelect(this.props.overview.category, this.saveCategory)} +

Story

+
+ {this.state.editingOverviewField === 'story' ? ( + this.makeEditingOverviewTextarea('story') + ) : ( + createPlaceholder(this.props.overview.story || '') + )} +
+
+

Filename

+
+ {this.state.editingOverviewField === 'fileName' ? ( + this.makeEditingOverviewTextarea('fileName') + ) : ( + createPlaceholder(this.props.overview.fileName || '') + )} +
+
+
+ ); } const createPlaceholder = (str: string): string => { if (str.match('^(\n| )*$')) { - return 'Enter Value Here.'; + return 'Enter Value Here (If Applicable)'; } else { return str; } @@ -180,3 +241,35 @@ const makeOverviewCardButton = (overview: IAssessmentOverview, listingPath: stri ); }; + +const assessmentCategoriesArr = [ + AssessmentCategories.Mission, + AssessmentCategories.Path, + AssessmentCategories.Sidequest, + AssessmentCategories.Contest +]; + +const categorySelect = ( + category: AssessmentCategory, + handleSelect = (i: AssessmentCategory, e: React.ChangeEvent) => {} +) => ( + + ); - private makeOptionsButton = () => - + ); private saveCategory = (i: AssessmentCategory, e: any) => { const overview = { @@ -203,21 +220,17 @@ export class EditingOverviewCard extends React.Component { {categorySelect(this.props.overview.category, this.saveCategory)}

Story

- {this.state.editingOverviewField === 'story' ? ( - this.makeEditingOverviewTextarea('story') - ) : ( - createPlaceholder(this.props.overview.story || '') - )} + {this.state.editingOverviewField === 'story' + ? this.makeEditingOverviewTextarea('story') + : createPlaceholder(this.props.overview.story || '')}
-
+

Filename

- {this.state.editingOverviewField === 'fileName' ? ( - this.makeEditingOverviewTextarea('fileName') - ) : ( - createPlaceholder(this.props.overview.fileName || '') - )} -
+ {this.state.editingOverviewField === 'fileName' + ? this.makeEditingOverviewTextarea('fileName') + : createPlaceholder(this.props.overview.fileName || '')} + ); @@ -243,7 +256,7 @@ const makeOverviewCardButton = (overview: IAssessmentOverview, listingPath: stri }; const assessmentCategoriesArr = [ - AssessmentCategories.Mission, + AssessmentCategories.Mission, AssessmentCategories.Path, AssessmentCategories.Sidequest, AssessmentCategories.Contest @@ -260,16 +273,13 @@ const categorySelect = ( itemRenderer={categoryRenderer} filterable={false} > - {this.manageQuestionTab(index)}
+
))} @@ -54,16 +55,26 @@ export class ManageQuestionTab extends React.Component { return (
{controlButton( - `Clone Question ${index + 1}`, + `Clone`, IconNames.DOCUMENT, this.confirmSave( this.makeQuestion(() => deepCopy(this.props.assessment.questions[index]), index) ) )} + {controlButton(`Delete`, IconNames.REMOVE, this.confirmSave(this.deleteQuestion(index)))} + {controlButton( + `Shift Up`, + IconNames.CARET_UP, + this.confirmSave(this.shiftQuestion(-1, index)), + {}, + index === 0 + )} {controlButton( - `Delete Question ${index + 1}`, - IconNames.REMOVE, - this.confirmSave(this.deleteQuestion(index)) + `Shift Down`, + IconNames.CARET_DOWN, + this.confirmSave(this.shiftQuestion(1, index)), + {}, + index >= this.props.assessment.questions.length - 1 )}
{controlButton( @@ -76,21 +87,6 @@ export class ManageQuestionTab extends React.Component { IconNames.CONFIRM, this.confirmSave(this.makeQuestion(mcqTemplate, index)) )} -
- {index > 0 - ? controlButton( - `Shift Question ${index + 1} Up`, - IconNames.CARET_UP, - this.confirmSave(this.shiftQuestion(-1, index)) - ) - : undefined} - {index < this.props.assessment.questions.length - 1 - ? controlButton( - `Shift Question ${index + 1} Down`, - IconNames.CARET_DOWN, - this.confirmSave(this.shiftQuestion(1, index)) - ) - : undefined}
); }; From c5e1344b004d14d6cfdcd4ed36147fa3ceb675dd Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Sun, 31 Mar 2019 23:27:17 +0800 Subject: [PATCH 24/26] small updates editor value now saved --- src/components/assessment/assessmentShape.ts | 1 + src/components/incubator/EditingWorkspace.tsx | 39 ++++++++++--------- .../ManageQuestionTab.tsx | 15 ------- src/components/workspace/ControlBar.tsx | 4 +- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/components/assessment/assessmentShape.ts b/src/components/assessment/assessmentShape.ts index 24598638f2..928b19d976 100644 --- a/src/components/assessment/assessmentShape.ts +++ b/src/components/assessment/assessmentShape.ts @@ -82,6 +82,7 @@ export interface IMCQQuestion extends IQuestion { export interface IQuestion { answer: string | number | null; + editorValue?: string | null; comment: string | null; content: string; id: number; diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index fb86e128f7..28878050b5 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -214,7 +214,6 @@ class AssessmentWorkspace extends React.Component { const assessment = retrieveLocalAssessment()!; - this.handleRefreshLibrary(); this.setState({ assessment, hasUnsavedChanges: false, @@ -222,6 +221,8 @@ class AssessmentWorkspace extends React.Component { - if (!this.state.editorPersist) { + private resetEditorValue = (checkPersist: boolean = true) => { + if (checkPersist || !this.state.editorPersist) { const question: IQuestion = this.state.assessment!.questions[this.formatedQuestionId()]; - const editorValue = - question.type === QuestionTypes.programming - ? ((question as IProgrammingQuestion).solutionTemplate as string) - : '//If you see this, this is a bug. Please report bug.'; + let editorValue: string; + if (question.type === QuestionTypes.programming) { + if (question.editorValue) { + editorValue = question.editorValue; + } else { + editorValue = ((question as IProgrammingQuestion).solutionTemplate as string); + } + } else { + editorValue = '//If you see this, this is a bug. Please report bug.'; + } + this.props.handleResetWorkspace({ editorValue }); this.props.handleEditorValueChange(editorValue); } }; private handleSave = () => { + const assessment = this.state.assessment!; + assessment.questions[this.formatedQuestionId()].editorValue = this.props.editorValue; this.setState({ + assessment, hasUnsavedChanges: false }); - storeLocalAssessment(this.state.assessment!); + storeLocalAssessment(assessment); // this.handleRefreshLibrary(); this.handleSaveGradeAndXp(); }; diff --git a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx index d5cb08fe28..42f443d78a 100644 --- a/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx +++ b/src/components/incubator/editingWorkspaceSideContent/ManageQuestionTab.tsx @@ -87,21 +87,6 @@ export class ManageQuestionTab extends React.Component { IconNames.CONFIRM, this.confirmSave(this.makeQuestion(mcqTemplate, index)) )} -
- {index > 0 - ? controlButton( - 'Shift Question Left', - IconNames.CARET_LEFT, - this.confirmSave(this.shiftQuestion(-1)) - ) - : undefined} - {index < this.props.assessment.questions.length - 1 - ? controlButton( - 'Shift Question Right', - IconNames.CARET_RIGHT, - this.confirmSave(this.shiftQuestion(1)) - ) - : undefined} ); }; diff --git a/src/components/workspace/ControlBar.tsx b/src/components/workspace/ControlBar.tsx index d982a595bf..390efd25cd 100644 --- a/src/components/workspace/ControlBar.tsx +++ b/src/components/workspace/ControlBar.tsx @@ -147,7 +147,7 @@ class ControlBar extends React.PureComponent { ? controlButton('Reset', IconNames.REPEAT, this.props.onClickReset) : undefined; const editorPersistSwitch = - this.props.handleToggleEditorPersist !== null + this.props.handleToggleEditorPersist !== undefined ? controlButton( 'Editor Persistence ' + (this.props.isEditorPersist ? 'Enabled' : 'Disabled'), this.props.isEditorPersist ? IconNames.TICK : IconNames.CROSS, @@ -205,7 +205,7 @@ class ControlBar extends React.PureComponent { ' editing mode.' } > - {controlButton('Switch editing mode', IconNames.REFRESH, this.props.toggleEditMode)} + {controlButton(this.props.editingMode + ' editing mode', IconNames.REFRESH, this.props.toggleEditMode)} ) : ( undefined From 4aa49ee1c9ca4517306811820044011a7a37438f Mon Sep 17 00:00:00 2001 From: Ger Hean Date: Tue, 2 Apr 2019 13:21:38 +0800 Subject: [PATCH 25/26] temp remove editor functionalites --- src/components/incubator/EditingWorkspace.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index 28878050b5..d36677a075 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -374,11 +374,11 @@ class AssessmentWorkspace extends React.Component { - this.setState({ - editorPersist: !this.state.editorPersist - }); - }; + // private toggleEditorPersist = () => { + // this.setState({ + // editorPersist: !this.state.editorPersist + // }); + // }; /** Pre-condition: IAssessment has been loaded */ private sideContentProps: (p: AssessmentWorkspaceProps, q: number) => SideContentProps = ( @@ -565,8 +565,8 @@ class AssessmentWorkspace extends React.Component Date: Tue, 2 Apr 2019 13:22:33 +0800 Subject: [PATCH 26/26] yarn format --- src/components/incubator/EditingWorkspace.tsx | 6 +++--- src/components/workspace/ControlBar.tsx | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/incubator/EditingWorkspace.tsx b/src/components/incubator/EditingWorkspace.tsx index d36677a075..81e08fbeba 100644 --- a/src/components/incubator/EditingWorkspace.tsx +++ b/src/components/incubator/EditingWorkspace.tsx @@ -292,7 +292,7 @@ class AssessmentWorkspace extends React.Component { - const assessment = this.state.assessment!; + const assessment = this.state.assessment!; assessment.questions[this.formatedQuestionId()].editorValue = this.props.editorValue; this.setState({ assessment, @@ -564,7 +564,7 @@ class AssessmentWorkspace extends React.Component { ' editing mode.' } > - {controlButton(this.props.editingMode + ' editing mode', IconNames.REFRESH, this.props.toggleEditMode)} + {controlButton( + this.props.editingMode + ' editing mode', + IconNames.REFRESH, + this.props.toggleEditMode + )} ) : ( undefined