Skip to content

CP3108 Staff Dashboard - Playground Default Source Version Selector #1073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/actions/__tests__/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
beginClearContext,
browseReplHistoryDown,
browseReplHistoryUp,
changeChapter,
changeEditorHeight,
changeEditorWidth,
changeExternalLibrary,
Expand All @@ -20,6 +21,7 @@ import {
evalRepl,
evalTestcase,
externalLibrarySelect,
fetchChapter,
highlightEditorLine,
moveCursor,
navigateToDeclaration,
Expand All @@ -29,6 +31,7 @@ import {
setEditorBreakpoint,
toggleEditorAutorun,
updateActiveTab,
updateChapter,
updateCurrentAssessmentId,
updateCurrentSubmissionId,
updateEditorValue,
Expand Down Expand Up @@ -417,3 +420,36 @@ test('moveCursor generates correct action object', () => {
}
});
});

test('fetchChapter generates correct action object', () => {
const action = fetchChapter();
expect(action).toEqual({
type: actionTypes.FETCH_CHAPTER
});
});

test('changeChapter generates correct action object', () => {
const chapter = 1;
const variant = 'default';
const action = changeChapter(chapter, variant);
expect(action).toEqual({
type: actionTypes.CHANGE_CHAPTER,
payload: {
chapter,
variant
}
});
});

test('updateChapter generates correct action object', () => {
const chapter = 1;
const variant = 'default';
const action = updateChapter(chapter, variant);
expect(action).toEqual({
type: actionTypes.UPDATE_CHAPTER,
payload: {
chapter,
variant
}
});
});
3 changes: 3 additions & 0 deletions src/actions/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export const UPDATE_EDITOR_BREAKPOINTS = 'UPDATE_EDITOR_BREAKPOINTS';
export const UPDATE_HAS_UNSAVED_CHANGES = 'UPDATE_HAS_UNSAVED_CHANGES';
export const UPDATE_REPL_VALUE = 'UPDATE_REPL_VALUE';
export const UPDATE_WORKSPACE = 'UPDATE_WORKSPACE';
export const FETCH_CHAPTER = 'FETCH_CHAPTER';
export const UPDATE_CHAPTER = 'UPDATE_CHAPTER';
export const CHANGE_CHAPTER = 'CHANGE_CHAPTER';

/** Collab Editing */
export const INIT_INVITE = 'INIT_INVITE';
Expand Down
8 changes: 8 additions & 0 deletions src/actions/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ export const updateHasUnsavedChanges = (
hasUnsavedChanges
});

export const fetchChapter = () => action(actionTypes.FETCH_CHAPTER);

export const changeChapter = (chapter: number, variant: Variant) =>
action(actionTypes.CHANGE_CHAPTER, { chapter, variant });

export const updateChapter = (chapter: number, variant: Variant) =>
action(actionTypes.UPDATE_CHAPTER, { chapter, variant });

export const promptAutocomplete = (
workspaceLocation: WorkspaceLocation,
row: number,
Expand Down
2 changes: 2 additions & 0 deletions src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface IDispatchProps {
handleDebuggerResume: () => void;
handleDebuggerReset: () => void;
handleToggleEditorAutorun: () => void;
handleFetchChapter: () => void;
handlePromptAutocomplete: (row: number, col: number, callback: any) => void;
}

Expand All @@ -111,6 +112,7 @@ class Playground extends React.Component<IPlaygroundProps, PlaygroundState> {
};
this.handlers.goGreen = this.toggleIsGreen.bind(this);
(window as any).thePlayground = this;
this.props.handleFetchChapter();
}

public render() {
Expand Down
1 change: 1 addition & 0 deletions src/components/__tests__/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const baseProps = {
handleDebuggerPause: () => {},
handleDebuggerResume: () => {},
handleDebuggerReset: () => {},
handleFetchChapter: () => {},
handlePromptAutocomplete: (row: number, col: number, callback: any) => {}
};

Expand Down
76 changes: 76 additions & 0 deletions src/components/academy/DefaultChapter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Button, Classes, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { ItemRenderer, Select } from '@blueprintjs/select';

import * as React from 'react';

import { RouteComponentProps } from 'react-router';

import { Variant } from 'js-slang/dist/types';
import { ISourceLanguage, sourceLanguages, styliseChapter } from '../../reducers/states';

export interface IChapterProps extends IDispatchProps, IStateProps, RouteComponentProps<{}> {}

export type IDispatchProps = {
handleFetchChapter: () => void;
handleUpdateChapter: (chapter: IChapter) => void;
handleChapterSelect?: (i: IChapter, e: React.ChangeEvent<HTMLSelectElement>) => void;
};

export interface IStateProps {
sourceChapter: number;
sourceVariant: Variant;
}

export interface IChapter {
chapter: number;
variant: Variant;
displayName: string;
}

export function DefaultChapter(props: IChapterProps) {
props.handleFetchChapter();

const chapters = sourceLanguages.map((lang: ISourceLanguage) => {
return {
chapter: lang.chapter,
variant: lang.variant,
displayName: styliseChapter(lang.chapter, lang.variant)
};
});

const chapterRenderer: ItemRenderer<IChapter> = (lang, { handleClick }) => (
<MenuItem
active={false}
key={lang.chapter + lang.variant}
onClick={handleClick}
text={lang.displayName}
/>
);

const ChapterSelectComponent = Select.ofType<IChapter>();

const chapSelect = (
currentChap: number,
currentVariant: Variant,
handleSelect = (i: IChapter) => {}
) => (
<ChapterSelectComponent
className={Classes.MINIMAL}
items={chapters}
onItemSelect={handleSelect}
itemRenderer={chapterRenderer}
filterable={false}
>
<Button
className={Classes.MINIMAL}
text={styliseChapter(currentChap, currentVariant)}
rightIcon={IconNames.DOUBLE_CARET_VERTICAL}
/>
</ChapterSelectComponent>
);

return (
<div> {chapSelect(props.sourceChapter, props.sourceVariant, props.handleUpdateChapter)} </div>
);
}
4 changes: 4 additions & 0 deletions src/components/academy/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { AssessmentCategories } from '../assessment/assessmentShape';
import NotificationBadge from '../../containers/notification/NotificationBadge';
import { filterNotificationsByType } from '../notification/NotificationHelpers';

import DefaultChapter from '../../containers/DefaultChapterContainer';

type OwnProps = {
role: Role;
};
Expand Down Expand Up @@ -81,6 +83,8 @@ const NavigationBar: React.SFC<OwnProps> = props => (
</NavbarGroup>
{props.role === Role.Admin || props.role === Role.Staff ? (
<NavbarGroup align={Alignment.RIGHT}>
<DefaultChapter />

<NavLink
to={'/academy/dashboard'}
activeClassName={Classes.ACTIVE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ exports[`Grading NavLink renders for Role.Admin 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<withRouter(Connect(DefaultChapter)) />
<NavLink to=\\"/academy/dashboard\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"globe\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down Expand Up @@ -148,6 +149,7 @@ exports[`Grading NavLink renders for Role.Staff 1`] = `
</NavLink>
</Blueprint3.NavbarGroup>
<Blueprint3.NavbarGroup align=\\"right\\">
<withRouter(Connect(DefaultChapter)) />
<NavLink to=\\"/academy/dashboard\\" activeClassName=\\"bp3-active\\" className=\\"NavigationBar__link bp3-button bp3-minimal\\" aria-current=\\"page\\">
<Blueprint3.Icon icon=\\"globe\\" />
<div className=\\"navbar-button-text hidden-xs\\">
Expand Down
30 changes: 30 additions & 0 deletions src/containers/DefaultChapterContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { withRouter } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';

import { changeChapter, fetchChapter } from '../actions';

import { DefaultChapter, IDispatchProps, IStateProps } from '../components/academy/DefaultChapter';
import { IChapter } from '../components/workspace/controlBar';
import { IState } from '../reducers/states';

const mapStateToProps: MapStateToProps<IStateProps, {}, IState> = state => ({
sourceChapter: state.workspaces.playground.context.chapter,
sourceVariant: state.workspaces.playground.context.variant
});

const mapDispatchToProps: MapDispatchToProps<IDispatchProps, {}> = (dispatch: Dispatch<any>) =>
bindActionCreators(
{
handleFetchChapter: () => fetchChapter(),
handleUpdateChapter: (chapter: IChapter) => changeChapter(chapter.chapter, chapter.variant)
},
dispatch
);

export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(DefaultChapter)
);
2 changes: 2 additions & 0 deletions src/containers/PlaygroundContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
evalEditor,
evalRepl,
externalLibrarySelect,
fetchChapter,
finishInvite,
generateLzString,
initInvite,
Expand Down Expand Up @@ -111,6 +112,7 @@ const mapDispatchToProps: MapDispatchToProps<IDispatchProps, {}> = (dispatch: Di
handleDebuggerPause: () => beginDebuggerPause(workspaceLocation),
handleDebuggerResume: () => debuggerResume(workspaceLocation),
handleDebuggerReset: () => debuggerReset(workspaceLocation),
handleFetchChapter: () => fetchChapter(),
handlePromptAutocomplete: (row: number, col: number, callback: any) =>
promptAutocomplete(workspaceLocation, row, col, callback)
},
Expand Down
14 changes: 14 additions & 0 deletions src/reducers/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SET_WEBSOCKET_STATUS,
TOGGLE_EDITOR_AUTORUN,
UPDATE_ACTIVE_TAB,
UPDATE_CHAPTER,
UPDATE_CURRENT_ASSESSMENT_ID,
UPDATE_CURRENT_SUBMISSION_ID,
UPDATE_EDITOR_VALUE,
Expand Down Expand Up @@ -656,6 +657,19 @@ export const reducer: Reducer<IWorkspaceManagerState> = (
hasUnsavedChanges: action.payload.hasUnsavedChanges
}
};
case UPDATE_CHAPTER:
return {
...state,
playground: {
...state.playground,
context: {
...state.playground.context,
chapter: action.payload.chapter,
variant: action.payload.variant
}
}
};

default:
return state;
}
Expand Down
17 changes: 17 additions & 0 deletions src/sagas/__tests__/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,23 @@ describe('Test NOTIFY_CHATKIT_USERS Action', () => {
});
});

describe('Test FETCH_CHAPTER Action', () => {
test('when chapter is obtained', () => {
return expectSaga(backendSaga)
.dispatch({ type: actionTypes.FETCH_CHAPTER })
.silentRun();
});
});

describe('Test CHANGE_CHAPTER Action', () => {
test('when chapter is changed', () => {
return expectSaga(backendSaga)
.withState({ session: { role: Role.Staff } })
.dispatch({ type: actionTypes.CHANGE_CHAPTER, payload: { chapterNo: 1, variant: 'default' } })
.silentRun();
});
});

describe('Test FETCH_GROUP_OVERVIEWS Action', () => {
test('when group overviews are obtained', () => {
return expectSaga(backendSaga)
Expand Down
28 changes: 28 additions & 0 deletions src/sagas/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,34 @@ function* backendSaga(): SagaIterator {
yield call(showSuccessMessage, 'Deleted successfully!', 1000);
});

yield takeEvery(actionTypes.FETCH_CHAPTER, function*() {
const chapter = yield call(request.fetchChapter);

if (chapter) {
yield put(actions.updateChapter(chapter.chapter.chapterno, chapter.chapter.variant));
}
});

yield takeEvery(actionTypes.CHANGE_CHAPTER, function*(
action: ReturnType<typeof actions.changeChapter>
) {
const tokens = yield select((state: IState) => ({
accessToken: state.session.accessToken,
refreshToken: state.session.refreshToken
}));

const chapter = action.payload;
const resp: Response = yield request.changeChapter(chapter.chapter, chapter.variant, tokens);

if (!resp || !resp.ok) {
yield request.handleResponseError(resp);
return;
}

yield put(actions.updateChapter(chapter.chapter, chapter.variant));
yield call(showSuccessMessage, 'Updated successfully!', 1000);
});

yield takeEvery(actionTypes.FETCH_GROUP_OVERVIEWS, function*(
action: ReturnType<typeof actions.fetchGroupOverviews>
) {
Expand Down
32 changes: 32 additions & 0 deletions src/sagas/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,38 @@ export async function getGroupOverviews(tokens: Tokens): Promise<IGroupOverview[
});
}

/**
* GET /chapter
*/
export async function fetchChapter(): Promise<Response | null> {
const resp = await request('chapter', 'GET', {
noHeaderAccept: true,
shouldAutoLogout: false,
shouldRefresh: true
});

if (!resp || !resp.ok) {
return null;
}

return await resp.json();
}

/**
* POST /chapter/update/1
*/
export async function changeChapter(chapterno: number, variant: string, tokens: Tokens) {
const resp = await request(`chapter/update/1`, 'POST', {
accessToken: tokens.accessToken,
body: { chapterno, variant },
noHeaderAccept: true,
refreshToken: tokens.refreshToken,
shouldAutoLogout: false,
shouldRefresh: true
});
return resp;
}

/**
* @returns {(Response|null)} Response if successful, otherwise null.
*
Expand Down