From 6965fb20b5357a2b58d7e08e072069c7bb65f6e2 Mon Sep 17 00:00:00 2001 From: Alden <49450743+alcen@users.noreply.github.com> Date: Wed, 17 Jul 2019 20:50:00 +0800 Subject: [PATCH 1/7] Modified Workspace SideContent component to use BlueprintJS tabs * SideContent and SideContentTabs now make use of BlueprintJS Tabs and Tab components * Tab buttons, icons are now larger * More properties added to SideContentProps, SideContentTab to accomodate extra functionality * Added documentation for these new properties * Changed 'icon' to 'iconName' for SideContentTab component for clarity * Instances of nested SideContentTabs on Mission Control feature were removed or replaced with dropdown menus * Updated CSS style names and 'alert' styles for SideControlTabs * Fixed redundant setInterval callback that removed 'alert' style from active Inspector tabs (now handled by CSS instead) * Removed (now redundant) 'activeTab' state and 'handleChangeActiveTab' actions from Workspace components * Updated tests --- .../externalLibs/env_visualizer/visualizer.js | 4 +- public/externalLibs/inspector/inspector.js | 12 +- src/actions/__tests__/workspaces.ts | 13 -- src/actions/actionTypes.ts | 1 - src/actions/workspaces.ts | 8 - src/components/Playground.tsx | 25 +-- src/components/__tests__/Playground.tsx | 1 - .../academy/grading/GradingWorkspace.tsx | 24 +-- .../assessment/AssessmentWorkspace.tsx | 44 ++--- .../__tests__/AssessmentWorkspace.tsx | 2 - .../missionControl/EditingWorkspace.tsx | 75 +++++---- .../DeploymentTab.tsx | 46 ++---- .../ProgrammingQuestionTemplateTab.tsx | 126 ++++++++++----- .../workspace/side-content/index.tsx | 152 +++++++++++++----- src/containers/PlaygroundContainer.ts | 3 - .../grading/GradingWorkspaceContainer.ts | 3 - .../AssessmentWorkspaceContainer.ts | 3 - .../EditingWorkspaceContainer.ts | 1 - src/reducers/__tests__/workspaces.ts | 20 --- src/reducers/workspaces.ts | 9 -- src/styles/_workspace.scss | 115 +++++++++---- 21 files changed, 373 insertions(+), 314 deletions(-) diff --git a/public/externalLibs/env_visualizer/visualizer.js b/public/externalLibs/env_visualizer/visualizer.js index 221977fe06..ac935be0e9 100644 --- a/public/externalLibs/env_visualizer/visualizer.js +++ b/public/externalLibs/env_visualizer/visualizer.js @@ -947,8 +947,8 @@ function draw_env(context) { // blink icon - const icon = document.getElementById("Env Visualizer-icon") - icon.classList.add("side-content-header-button-alert") + const icon = document.getElementById("env-icon") + icon.classList.add("side-content-tab-alert") // reset current drawing fnObjectLayer.scene.clear() diff --git a/public/externalLibs/inspector/inspector.js b/public/externalLibs/inspector/inspector.js index 3578ae9044..f70bdd56fc 100644 --- a/public/externalLibs/inspector/inspector.js +++ b/public/externalLibs/inspector/inspector.js @@ -107,12 +107,6 @@ "Symbol(Used to implement hoisting)": " " } - setInterval(()=>{ - if(document.getElementById("inspector-container") != null){ - document.getElementById("Inspector-icon").classList.remove("side-content-header-button-alert"); - } - },1000) - function updateContext(context, stringify) { function dumpTable(env) { var res = ''; @@ -127,9 +121,9 @@ } // blinks icon - const icon = document.getElementById("Inspector-icon"); + const icon = document.getElementById("inspector-icon"); if (!context) { - icon.classList.remove("side-content-header-button-alert"); + icon.classList.remove("side-content-tab-alert"); return; } @@ -148,7 +142,7 @@ tbody.innerHTML = "
" + frames[i].name + "" + envtoString newtable.appendChild(tbody) container.appendChild(newtable) - icon.classList.add("side-content-header-button-alert"); + icon.classList.add("side-content-tab-alert"); } } catch (e) { container.innerHTML = e diff --git a/src/actions/__tests__/workspaces.ts b/src/actions/__tests__/workspaces.ts index f7ed00384b..dfe0c77497 100644 --- a/src/actions/__tests__/workspaces.ts +++ b/src/actions/__tests__/workspaces.ts @@ -6,7 +6,6 @@ import { beginClearContext, browseReplHistoryDown, browseReplHistoryUp, - changeActiveTab, changeEditorHeight, changeEditorWidth, changePlaygroundExternal, @@ -55,18 +54,6 @@ test('browseReplHistoryUp generates correct action object', () => { }); }); -test('changeActiveTab generates correct action object', () => { - const activeTab = 3; - const action = changeActiveTab(activeTab, playgroundWorkspace); - expect(action).toEqual({ - type: actionTypes.CHANGE_ACTIVE_TAB, - payload: { - activeTab, - workspaceLocation: playgroundWorkspace - } - }); -}); - test('changePlaygroundExternal generates correct action object', () => { const newExternal = 'new-external-test'; const action = changePlaygroundExternal(newExternal); diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index aab6d01184..18cc753efe 100644 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -32,7 +32,6 @@ export const HIGHLIGHT_LINE = 'HIGHLIGHT_LINE'; export const BEGIN_CLEAR_CONTEXT = 'BEGIN_CLEAR_CONTEXT'; export const BROWSE_REPL_HISTORY_DOWN = 'BROWSE_REPL_HISTORY_DOWN'; export const BROWSE_REPL_HISTORY_UP = 'BROWSE_REPL_HISTORY_UP'; -export const CHANGE_ACTIVE_TAB = 'CHANGE_ACTIVE_TAB'; export const CHANGE_EDITOR_HEIGHT = 'CHANGE_EDITOR_HEIGHT'; export const CHANGE_EDITOR_WIDTH = 'CHANGE_EDITOR_WIDTH'; export const CHANGE_PLAYGROUND_EXTERNAL = 'CHANGE_PLAYGROUND_EXTERNAL'; diff --git a/src/actions/workspaces.ts b/src/actions/workspaces.ts index 22a5b29c56..102db7c053 100755 --- a/src/actions/workspaces.ts +++ b/src/actions/workspaces.ts @@ -36,14 +36,6 @@ export const browseReplHistoryUp: ActionCreator = ( payload: { workspaceLocation } }); -export const changeActiveTab: ActionCreator = ( - activeTab: number, - workspaceLocation: WorkspaceLocation -) => ({ - type: actionTypes.CHANGE_ACTIVE_TAB, - payload: { activeTab, workspaceLocation } -}); - export const changePlaygroundExternal: ActionCreator = ( newExternal: string ) => ({ diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index f657542e36..9f8a9f4795 100755 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -37,7 +37,6 @@ the REPL. export interface IPlaygroundProps extends IDispatchProps, IStateProps, RouteComponentProps<{}> {} export interface IStateProps { - activeTab: number; editorSessionId: string; editorValue: string; editorHeight?: number; @@ -60,7 +59,6 @@ export interface IStateProps { export interface IDispatchProps { handleBrowseHistoryDown: () => void; handleBrowseHistoryUp: () => void; - handleChangeActiveTab: (activeTab: number) => void; handleChapterSelect: (chapter: number) => void; handleEditorEval: () => void; handleEditorHeightChange: (height: number) => void; @@ -162,8 +160,7 @@ class Playground extends React.Component { }, sideContentHeight: this.props.sideContentHeight, sideContentProps: { - activeTab: this.props.activeTab, - handleChangeActiveTab: this.props.handleChangeActiveTab, + defaultSelectedTabId: 'introduction', tabs: [playgroundIntroductionTab, listVisualizerTab, inspectorTab, envVisualizerTab] } }; @@ -189,26 +186,30 @@ class Playground extends React.Component { const playgroundIntroductionTab: SideContentTab = { label: 'Introduction', - icon: IconNames.COMPASS, - body: + iconName: IconNames.COMPASS, + body: , + id: 'introduction' }; const listVisualizerTab: SideContentTab = { label: 'Data Visualizer', - icon: IconNames.EYE_OPEN, - body: + iconName: IconNames.EYE_OPEN, + body: , + id: 'data' }; const inspectorTab: SideContentTab = { label: 'Inspector', - icon: IconNames.SEARCH, - body: + iconName: IconNames.SEARCH, + body: , + id: 'inspector' }; const envVisualizerTab: SideContentTab = { label: 'Env Visualizer', - icon: IconNames.GLOBE, - body: + iconName: IconNames.GLOBE, + body: , + id: 'env' }; export default Playground; diff --git a/src/components/__tests__/Playground.tsx b/src/components/__tests__/Playground.tsx index 665802b21b..e78ee462f8 100755 --- a/src/components/__tests__/Playground.tsx +++ b/src/components/__tests__/Playground.tsx @@ -12,7 +12,6 @@ const baseProps = { isRunning: false, isDebugging: false, enableDebugging: true, - activeTab: 0, editorSessionId: '', editorWidth: '50%', isEditorAutorun: false, diff --git a/src/components/academy/grading/GradingWorkspace.tsx b/src/components/academy/grading/GradingWorkspace.tsx index 71a2e9396b..1e4f20c9e4 100755 --- a/src/components/academy/grading/GradingWorkspace.tsx +++ b/src/components/academy/grading/GradingWorkspace.tsx @@ -26,7 +26,6 @@ import { Grading, IAnsweredQuestion } from './gradingShape'; export type GradingWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { - activeTab: number; autogradingResults: AutogradingResult[]; grading?: Grading; editorPrepend: string; @@ -56,7 +55,6 @@ export type OwnProps = { export type DispatchProps = { handleBrowseHistoryDown: () => void; handleBrowseHistoryUp: () => void; - handleChangeActiveTab: (activeTab: number) => void; handleChapterSelect: (chapter: any, changeEvent: any) => void; handleClearContext: (library: Library) => void; handleEditorEval: () => void; @@ -245,12 +243,10 @@ class GradingWorkspace extends React.Component { props: GradingWorkspaceProps, questionId: number ) => ({ - activeTab: props.activeTab, - handleChangeActiveTab: props.handleChangeActiveTab, tabs: [ { label: `Grading: Question ${questionId}`, - icon: IconNames.TICK, + iconName: IconNames.TICK, /* Render an editor with the xp given to the current question. */ body: ( { maxXp={props.grading![questionId].question.maxXp} studentName={props.grading![questionId].student.name} /> - ) + ), + id: 'grading' }, { label: `Task ${questionId + 1}`, - icon: IconNames.NINJA, - body: + iconName: IconNames.NINJA, + body: , + id: 'question_overview' }, { label: `Chat`, - icon: IconNames.CHAT, + iconName: IconNames.CHAT, body: USE_CHATKIT ? ( ) : ( ChatKit disabled. - ) + ), + id: 'chat' }, { label: `Autograder`, - icon: IconNames.AIRPLANE, + iconName: IconNames.AIRPLANE, body: ( - ) + ), + id: 'autograder' } ] }); diff --git a/src/components/assessment/AssessmentWorkspace.tsx b/src/components/assessment/AssessmentWorkspace.tsx index 6950821728..8946dc2c9d 100755 --- a/src/components/assessment/AssessmentWorkspace.tsx +++ b/src/components/assessment/AssessmentWorkspace.tsx @@ -21,7 +21,7 @@ import { controlButton } from '../commons'; import Markdown from '../commons/Markdown'; import Workspace, { WorkspaceProps } from '../workspace'; import { ControlBarProps } from '../workspace/ControlBar'; -import { SideContentProps } from '../workspace/side-content'; +import { SideContentProps, SideContentTab } from '../workspace/side-content'; import Autograder from '../workspace/side-content/Autograder'; import ToneMatrix from '../workspace/side-content/ToneMatrix'; import { @@ -39,7 +39,6 @@ import GradingResult from './GradingResult'; export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { - activeTab: number; assessment?: IAssessment; autogradingResults: AutogradingResult[]; editorPrepend: string; @@ -72,7 +71,6 @@ export type DispatchProps = { handleAssessmentFetch: (assessmentId: number) => void; handleBrowseHistoryDown: () => void; handleBrowseHistoryUp: () => void; - handleChangeActiveTab: (activeTab: number) => void; handleChapterSelect: (chapter: any, changeEvent: any) => void; handleClearContext: (library: Library) => void; handleEditorEval: () => void; @@ -333,27 +331,30 @@ class AssessmentWorkspace extends React.Component< props: AssessmentWorkspaceProps, questionId: number ) => { - const tabs = [ + const tabs: SideContentTab[] = [ { label: `Task ${questionId + 1}`, - icon: IconNames.NINJA, - body: + iconName: IconNames.NINJA, + body: , + id: 'question_overview' }, { label: `${props.assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, - body: + iconName: IconNames.BRIEFCASE, + body: , + id: 'briefing' }, { label: `${props.assessment!.category} Autograder`, - icon: IconNames.AIRPLANE, + iconName: IconNames.AIRPLANE, body: ( - ) + ), + id: 'autograder' } ]; const isGraded = props.assessment!.questions[questionId].grader !== null; @@ -361,7 +362,7 @@ class AssessmentWorkspace extends React.Component< tabs.push( { label: `Grading`, - icon: IconNames.TICK, + iconName: IconNames.TICK, body: ( - ) + ), + id: 'grading' }, { - label: `Comments`, - icon: IconNames.CHAT, + label: `Chat`, + iconName: IconNames.CHAT, body: USE_CHATKIT ? ( ) : ( Chatkit disabled. - ) + ), + id: 'chat' } ); } @@ -389,15 +392,12 @@ class AssessmentWorkspace extends React.Component< if (functionsAttached.includes('get_matrix')) { tabs.push({ label: `Tone Matrix`, - icon: IconNames.GRID_VIEW, - body: + iconName: IconNames.GRID_VIEW, + body: , + id: 'tone_matrix' }); } - return { - activeTab: props.activeTab, - handleChangeActiveTab: props.handleChangeActiveTab, - tabs - }; + return { tabs }; }; /** Pre-condition: IAssessment has been loaded */ diff --git a/src/components/assessment/__tests__/AssessmentWorkspace.tsx b/src/components/assessment/__tests__/AssessmentWorkspace.tsx index 56b04e9fd4..85c7bc66d5 100644 --- a/src/components/assessment/__tests__/AssessmentWorkspace.tsx +++ b/src/components/assessment/__tests__/AssessmentWorkspace.tsx @@ -6,7 +6,6 @@ import { Library } from '../assessmentShape'; import AssessmentWorkspace, { AssessmentWorkspaceProps } from '../AssessmentWorkspace'; const defaultProps: AssessmentWorkspaceProps = { - activeTab: 0, assessmentId: 0, autogradingResults: [], notAttempted: true, @@ -22,7 +21,6 @@ const defaultProps: AssessmentWorkspaceProps = { handleAssessmentFetch: (assessmentId: number) => {}, handleBrowseHistoryDown: () => {}, handleBrowseHistoryUp: () => {}, - handleChangeActiveTab: (activeTab: number) => {}, handleChapterSelect: (chapter: any, changeEvent: any) => {}, handleClearContext: (library: Library) => {}, handleEditorEval: () => {}, diff --git a/src/components/missionControl/EditingWorkspace.tsx b/src/components/missionControl/EditingWorkspace.tsx index 3a2b0d1058..64aaa8025a 100644 --- a/src/components/missionControl/EditingWorkspace.tsx +++ b/src/components/missionControl/EditingWorkspace.tsx @@ -19,7 +19,7 @@ import { controlButton } from '../commons'; import Markdown from '../commons/Markdown'; import Workspace, { WorkspaceProps } from '../workspace'; import { ControlBarProps } from '../workspace/ControlBar'; -import { SideContentProps } from '../workspace/side-content'; +import { SideContentProps, SideContentTab } from '../workspace/side-content'; import ToneMatrix from '../workspace/side-content/ToneMatrix'; import { AutograderTab, @@ -39,7 +39,6 @@ import { export type AssessmentWorkspaceProps = DispatchProps & OwnProps & StateProps; export type StateProps = { - activeTab: number; editorHeight?: number; editorValue: string | null; editorWidth: string; @@ -93,7 +92,6 @@ export type DispatchProps = { interface IState { assessment: IAssessment | null; - activeTab: number; editingMode: string; hasUnsavedChanges: boolean; showResetTemplateOverlay: boolean; @@ -106,7 +104,6 @@ class AssessmentWorkspace extends React.Component { - this.setState({ - activeTab: tab - }); - }; - private toggleEditingMode = () => { const toggle = this.state.editingMode === 'question' ? 'global' : 'question'; this.setState({ - activeTab: 0, editingMode: toggle }); }; @@ -416,7 +406,7 @@ class AssessmentWorkspace extends React.Component { const assessment = this.state.assessment!; - let tabs; + let tabs: SideContentTab[]; if (this.state.editingMode === 'question') { const qnType = this.state.assessment!.questions[this.props.questionId].type; const questionTemplateTab = @@ -440,23 +430,25 @@ class AssessmentWorkspace extends React.Component - ) + ), + id: 'question_overview' }, { label: `Question Template`, - icon: IconNames.DOCUMENT, - body: questionTemplateTab + iconName: IconNames.DOCUMENT, + body: questionTemplateTab, + id: 'question_template' }, { label: `Manage Local Deployment`, - icon: IconNames.HOME, + iconName: IconNames.HOME, body: ( - ) + ), + id: 'local_deployment' }, { label: `Manage Local Grader Deployment`, - icon: IconNames.CONFIRM, + iconName: IconNames.CONFIRM, body: ( - ) + ), + id: 'local_grader_deployment' }, { label: `Grading`, - icon: IconNames.TICK, + iconName: IconNames.TICK, body: ( - ) + ), + id: 'grading' } ]; if (qnType === 'programming') { tabs.push({ label: `Autograder`, - icon: IconNames.AIRPLANE, + iconName: IconNames.AIRPLANE, body: ( - ) + ), + id: 'autograder' }); } const functionsAttached = assessment!.questions[questionId].library.external.symbols; if (functionsAttached.includes('get_matrix')) { tabs.push({ label: `Tone Matrix`, - icon: IconNames.GRID_VIEW, - body: + iconName: IconNames.GRID_VIEW, + body: , + id: 'tone_matrix' }); } } else { tabs = [ { label: `${assessment!.category} Briefing`, - icon: IconNames.BRIEFCASE, + iconName: IconNames.BRIEFCASE, body: ( - ) + ), + id: 'briefing' }, { label: `Manage Question`, - icon: IconNames.WRENCH, + iconName: IconNames.WRENCH, body: ( - ) + ), + id: 'manage_question' }, { label: `Manage Global Deployment`, - icon: IconNames.GLOBE, + iconName: IconNames.GLOBE, body: ( - ) + ), + id: 'global_deployment' }, { label: `Manage Global Grader Deployment`, - icon: IconNames.CONFIRM, + iconName: IconNames.CONFIRM, body: ( - ) + ), + id: 'global_grader_deployment' } ]; } - return { - activeTab: this.state.activeTab, - handleChangeActiveTab: this.handleChangeActiveTab, - tabs - }; + return { tabs }; }; /** Pre-condition: IAssessment has been loaded */ diff --git a/src/components/missionControl/editingWorkspaceSideContent/DeploymentTab.tsx b/src/components/missionControl/editingWorkspaceSideContent/DeploymentTab.tsx index 2225a409f3..dd2a327701 100644 --- a/src/components/missionControl/editingWorkspaceSideContent/DeploymentTab.tsx +++ b/src/components/missionControl/editingWorkspaceSideContent/DeploymentTab.tsx @@ -1,4 +1,4 @@ -import { Button, Classes, MenuItem, Switch } from '@blueprintjs/core'; +import { Button, Classes, Divider, MenuItem, Switch } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { ItemRenderer, Select } from '@blueprintjs/select'; import * as React from 'react'; @@ -7,7 +7,6 @@ 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'; @@ -33,14 +32,7 @@ interface IExternal { symbols: string[]; } -export class DeploymentTab extends React.Component { - public constructor(props: IProps) { - super(props); - this.state = { - activeTab: 0 - }; - } - +export class DeploymentTab extends React.Component { public render() { if (!this.props.isOptionalDeployment) { return ( @@ -98,8 +90,7 @@ export class DeploymentTab extends React.Component {externalSelect(deployment.external.name, this.handleExternalSelect!)} -
-
+
Symbols:

@@ -120,43 +111,24 @@ export class DeploymentTab extends React.Component ); - 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)} - + + {symbolsFragment} + + {globalsFragment}
); }; - private handleChangeActiveTab = (tab: number) => { - this.setState({ - activeTab: tab - }); - }; - private textareaContent = (path: Array) => { return ( ) => void; } -interface IState { - activeTab: number; +interface IQuestionEditorState { + activeEditor: QuestionEditor; templateValue: string; templateFocused: boolean; } -const tabPaths = ['prepend', 'postpend', 'solutionTemplate', 'answer']; +const questionEditorPaths = ['prepend', 'postpend', 'solutionTemplate', 'answer'] as const; + +export type QuestionEditorId = typeof questionEditorPaths[number]; + +const QuestionEditorSelect = Select.ofType(); + +export type QuestionEditor = { + label: string, + icon: IconName, + id: QuestionEditorId +}; + +const questionEditors: QuestionEditor[] = [ + { + label: 'Prepend', + icon: IconNames.CHEVRON_UP, + id: 'prepend' + }, + { + label: 'Postpend', + icon: IconNames.CHEVRON_DOWN, + id: 'postpend' + }, + { + label: 'Solution Template', + icon: IconNames.MANUAL, + id: 'solutionTemplate' + }, + { + label: 'Suggested Answer', + icon: IconNames.TICK, + id: 'answer' + } +]; -export class ProgrammingQuestionTemplateTab extends React.Component { - public constructor(props: IProps) { +/* + * activeEditor is the default editor to show initially + */ +export class ProgrammingQuestionTemplateTab extends React.Component { + public constructor(props: IQuestionEditorProps) { super(props); this.state = { - activeTab: 0, + activeEditor: questionEditors[0], templateValue: '', templateFocused: false }; @@ -42,7 +78,7 @@ export class ProgrammingQuestionTemplateTab extends React.Component { const qnPath = ['questions', this.props.questionId]; - const path = qnPath.concat([tabPaths[this.state.activeTab]]); + const path = qnPath.concat(this.state.activeEditor.id); const copyFromEditorButton = controlButton( 'Copy from Editor', @@ -56,45 +92,47 @@ export class ProgrammingQuestionTemplateTab extends React.Component {copyFromEditorButton} {copyToEditorButton} -
-
+ {this.editor(path)} ); - const tabs = [ - { - label: `Prepend`, - icon: IconNames.CHEVRON_UP, - body: tabComponent - }, - { - label: `Postpend`, - icon: IconNames.CHEVRON_DOWN, - body: tabComponent - }, - { - label: `Solution Template`, - icon: IconNames.MANUAL, - body: tabComponent - }, - { - label: `Suggested Answer`, - icon: IconNames.TICK, - body: tabComponent - } - ]; + const menuRenderer: ItemRenderer = (editor, { handleClick }) => ( + + ); + + const editorSelect = ( + currentEditor: QuestionEditor, + handleSelect: (i: QuestionEditor) => void + ) => ( + +