From 361ea934bae7765af3f67d819fb4303d473f615f Mon Sep 17 00:00:00 2001 From: ning Date: Mon, 9 Jul 2018 14:33:22 +0800 Subject: [PATCH 1/7] Add fetching of user role --- src/actions/actionTypes.ts | 1 + src/actions/session.ts | 7 +++++++ src/reducers/session.ts | 6 ++++++ src/reducers/states.ts | 7 +++++++ src/sagas/backend.ts | 9 +++++---- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts index e3f754e31b..beaad885eb 100644 --- a/src/actions/actionTypes.ts +++ b/src/actions/actionTypes.ts @@ -46,6 +46,7 @@ export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS' export const FETCH_GRADING = 'FETCH_GRADING' export const FETCH_GRADING_OVERVIEWS = 'FETCH_GRADING_OVERVIEWS' export const LOGIN = 'LOGIN' +export const SET_ROLE = 'SET_ROLE' export const SET_TOKENS = 'SET_TOKENS' export const SET_USERNAME = 'SET_USERNAME' export const UPDATE_HISTORY_HELPERS = 'UPDATE_HISTORY_HELPERS' diff --git a/src/actions/session.ts b/src/actions/session.ts index f309ec26a3..9007294f2c 100644 --- a/src/actions/session.ts +++ b/src/actions/session.ts @@ -4,6 +4,8 @@ import { Grading, GradingOverview } from '../components/academy/grading/gradingS import { IAssessment, IAssessmentOverview } from '../components/assessment/assessmentShape' import * as actionTypes from './actionTypes' +import { Role } from '../reducers/states' + export const fetchAuth: ActionCreator = (ivleToken: string) => ({ type: actionTypes.FETCH_AUTH, payload: ivleToken @@ -35,6 +37,11 @@ export const login = () => ({ type: actionTypes.LOGIN }) +export const setRole: ActionCreator = (role: Role) => ({ + type: actionTypes.SET_ROLE, + payload: role +}) + export const setTokens: ActionCreator = ({ accessToken, refreshToken }) => ({ type: actionTypes.SET_TOKENS, payload: { diff --git a/src/reducers/session.ts b/src/reducers/session.ts index 91a04ec6aa..2cc7c722c9 100644 --- a/src/reducers/session.ts +++ b/src/reducers/session.ts @@ -2,6 +2,7 @@ import { Reducer } from 'redux' import { IAction, + SET_ROLE, SET_TOKENS, SET_USERNAME, UPDATE_ASSESSMENT, @@ -14,6 +15,11 @@ import { defaultSession, ISessionState } from './states' export const reducer: Reducer = (state = defaultSession, action: IAction) => { switch (action.type) { + case SET_ROLE: + return { + ...state, + role: action.payload + } case SET_TOKENS: return { ...state, diff --git a/src/reducers/states.ts b/src/reducers/states.ts index a776c6b7a0..cd3edf4739 100644 --- a/src/reducers/states.ts +++ b/src/reducers/states.ts @@ -58,6 +58,7 @@ export interface ISessionState { readonly gradings: Map readonly historyHelper: HistoryHelper readonly refreshToken?: string + readonly role?: Role readonly storyAct: string readonly username?: string } @@ -114,6 +115,12 @@ export enum ApplicationEnvironment { Test = 'test' } +export enum Role { + Student = 'student', + Staff = 'staff', + Admin = 'admin' +} + export const sourceChapters = [1, 2] const latestSourceChapter = sourceChapters.slice(-1)[0] diff --git a/src/sagas/backend.ts b/src/sagas/backend.ts index a91e9b9f52..9700ec8ce9 100644 --- a/src/sagas/backend.ts +++ b/src/sagas/backend.ts @@ -21,15 +21,16 @@ function* backendSaga(): SagaIterator { accessToken: resp.access_token, refreshToken: resp.refresh_token } - const username = yield getUsername(tokens.accessToken) + const user = yield getUser(tokens.accessToken) yield put(actions.setTokens(tokens)) - yield put(actions.setUsername(username)) + yield put(actions.setRole(user.role)) + yield put(actions.setUsername(user.name)) yield delay(2000) yield history.push('/academy') }) } -function* getUsername(accessToken: string) { +function* getUser(accessToken: string) { const resp = yield call(request, 'user', { method: 'GET', headers: new Headers({ @@ -37,7 +38,7 @@ function* getUsername(accessToken: string) { Accept: 'application/json' }) }) - return resp.name + return resp } function request(path: string, opts: {}) { From e12ee57fa1b6a9231c8b9c67568cf5ed338fb874 Mon Sep 17 00:00:00 2001 From: ning Date: Mon, 9 Jul 2018 14:36:48 +0800 Subject: [PATCH 2/7] Remove route /status --- src/components/Application.tsx | 1 - src/components/NavigationBar.tsx | 10 ++-------- .../__tests__/__snapshots__/Application.tsx.snap | 1 - .../__tests__/__snapshots__/NavigationBar.tsx.snap | 10 ++++------ 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/components/Application.tsx b/src/components/Application.tsx index 19b9b4f44e..68d2d932e7 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -36,7 +36,6 @@ const Application: React.SFC = props => { - diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 1e1806a72e..52bf40b734 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -66,14 +66,8 @@ const NavigationBar: React.SFC = props => (
- - -
{titleCase(props.username)}
-
+ +
{titleCase(props.username)}
)} diff --git a/src/components/__tests__/__snapshots__/Application.tsx.snap b/src/components/__tests__/__snapshots__/Application.tsx.snap index 4a05d50af1..37dc25c29e 100644 --- a/src/components/__tests__/__snapshots__/Application.tsx.snap +++ b/src/components/__tests__/__snapshots__/Application.tsx.snap @@ -9,7 +9,6 @@ exports[`Application renders correctly 1`] = ` - diff --git a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap index 8503a80cce..7414630211 100644 --- a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -69,12 +69,10 @@ exports[`NavigationBar renders correctly with username 1`] = `
- - -
- Evis Rucer -
-
+ +
+ Evis Rucer +
" From 1174bae4f1af75090b35404e1438c6f9d550268e Mon Sep 17 00:00:00 2001 From: ning Date: Mon, 9 Jul 2018 14:47:25 +0800 Subject: [PATCH 3/7] Abstract a Status component for username in navbar --- src/components/NavigationBar.tsx | 26 ++++------------ .../__snapshots__/NavigationBar.tsx.snap | 13 +------- src/components/academy/Status.tsx | 30 +++++++++++++++++++ src/styles/_navigationBar.scss | 4 +++ 4 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 src/components/academy/Status.tsx diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 52bf40b734..8a005de983 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -1,15 +1,11 @@ -import { - Alignment, - Icon, - Navbar, - NavbarDivider, - NavbarGroup, - NavbarHeading -} from '@blueprintjs/core' +import { Alignment, Icon, Navbar, NavbarGroup, NavbarHeading } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons' import * as React from 'react' import { NavLink } from 'react-router-dom' +import { Role } from '../reducers/states' +import Status from './academy/Status' + export interface INavigationBarProps { title: string username?: string @@ -59,22 +55,10 @@ const NavigationBar: React.SFC = props => ( {props.username === undefined ? ( undefined ) : ( - <> -
- -
-
- -
- -
{titleCase(props.username)}
- + )} ) -const titleCase = (str: string) => - str.replace(/\w\S*/g, wrd => wrd.charAt(0).toUpperCase() + wrd.substr(1).toLowerCase()) - export default NavigationBar diff --git a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap index 7414630211..84fdca70db 100644 --- a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -62,18 +62,7 @@ exports[`NavigationBar renders correctly with username 1`] = ` Playground - -
- -
-
- -
- -
- Evis Rucer -
-
+ " `; diff --git a/src/components/academy/Status.tsx b/src/components/academy/Status.tsx new file mode 100644 index 0000000000..efd481c330 --- /dev/null +++ b/src/components/academy/Status.tsx @@ -0,0 +1,30 @@ +import { Icon, NavbarDivider } from '@blueprintjs/core' +import { IconNames } from '@blueprintjs/icons' +import * as React from 'react' + +import { Role } from '../../reducers/states' + +type StatusProps = OwnProps + +type OwnProps = { + username: string + role: Role +} + +const Status: React.SFC = props => ( + <> +
+ +
+
+ +
+ +
{titleCase(props.username)}
+ +) + +const titleCase = (str: string) => + str.replace(/\w\S*/g, wrd => wrd.charAt(0).toUpperCase() + wrd.substr(1).toLowerCase()) + +export default Status diff --git a/src/styles/_navigationBar.scss b/src/styles/_navigationBar.scss index f86c1ba25c..f2abe69a4d 100644 --- a/src/styles/_navigationBar.scss +++ b/src/styles/_navigationBar.scss @@ -33,6 +33,10 @@ .pt-navbar-heading { text-transform: uppercase; } + + .navbar-username { + margin-left: 0.4rem; + } } /* From 809f007285db9eed1997a291f3a83109a9a4daf2 Mon Sep 17 00:00:00 2001 From: ning Date: Mon, 9 Jul 2018 15:07:50 +0800 Subject: [PATCH 4/7] Make Status a Popover --- src/components/academy/Status.tsx | 13 ++++++++++--- src/styles/_navigationBar.scss | 8 +++++++- src/styles/_workspace.scss | 6 +++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/academy/Status.tsx b/src/components/academy/Status.tsx index efd481c330..cadda584e9 100644 --- a/src/components/academy/Status.tsx +++ b/src/components/academy/Status.tsx @@ -1,8 +1,9 @@ -import { Icon, NavbarDivider } from '@blueprintjs/core' +import { NavbarDivider, Popover, Text } from '@blueprintjs/core' import { IconNames } from '@blueprintjs/icons' import * as React from 'react' import { Role } from '../../reducers/states' +import { controlButton } from '../commons' type StatusProps = OwnProps @@ -19,8 +20,14 @@ const Status: React.SFC = props => (
- -
{titleCase(props.username)}
+ +
+ {controlButton(titleCase(props.username), IconNames.USER)} +
+ +

{`Source Academy, ${titleCase(props.role)}`}

+
+
) diff --git a/src/styles/_navigationBar.scss b/src/styles/_navigationBar.scss index f2abe69a4d..ecadca6df0 100644 --- a/src/styles/_navigationBar.scss +++ b/src/styles/_navigationBar.scss @@ -35,7 +35,13 @@ } .navbar-username { - margin-left: 0.4rem; + margin-left: 0; + } +} + +.Popover-share { + h4 { + margin: 0.3rem 0 0.3rem 0; } } diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index d12e03d853..a16a210ae6 100644 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -276,7 +276,7 @@ $code-color-error: #ff4444; .pt-popover-content { background: $cadet-color-4; display: flex; - padding: 0.4rem 0.4rem 0.4rem 0.8rem; + padding: 0.4rem 0.8rem 0.4rem 0.8rem; input { width: 25rem; @@ -285,6 +285,10 @@ $code-color-error: #ff4444; outline: none; } } + + button { + padding: 5px 5px 5px 10px; + } } } From 3226f6cd06154eedee53e28cfb6091cc189a8990 Mon Sep 17 00:00:00 2001 From: ning Date: Mon, 9 Jul 2018 15:11:34 +0800 Subject: [PATCH 5/7] Connect components to state.session.role --- src/components/Application.tsx | 5 +++-- src/components/NavigationBar.tsx | 7 ++++--- .../__tests__/__snapshots__/Application.tsx.snap | 2 +- .../__tests__/__snapshots__/NavigationBar.tsx.snap | 1 - src/containers/ApplicationContainer.ts | 1 + 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/Application.tsx b/src/components/Application.tsx index 68d2d932e7..f951163051 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -7,13 +7,14 @@ import Academy from '../containers/academy' import Announcements from '../containers/AnnouncementsContainer' import Login from '../containers/LoginContainer' import Playground from '../containers/PlaygroundContainer' -import { sourceChapters } from '../reducers/states' +import { Role, sourceChapters } from '../reducers/states' import NavigationBar from './NavigationBar' import NotFound from './NotFound' export interface IApplicationProps extends IDispatchProps, RouteComponentProps<{}> { title: string accessToken?: string + role?: Role username?: string } @@ -29,7 +30,7 @@ const Application: React.SFC = props => { return (
- +
diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index 8a005de983..cdf4b96512 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -9,6 +9,7 @@ import Status from './academy/Status' export interface INavigationBarProps { title: string username?: string + role?: Role } const NavigationBar: React.SFC = props => ( @@ -52,10 +53,10 @@ const NavigationBar: React.SFC = props => (
Playground
- {props.username === undefined ? ( - undefined + {props.username !== undefined && props.role !== undefined ? ( + ) : ( - + undefined )} diff --git a/src/components/__tests__/__snapshots__/Application.tsx.snap b/src/components/__tests__/__snapshots__/Application.tsx.snap index 37dc25c29e..8b1788eb9d 100644 --- a/src/components/__tests__/__snapshots__/Application.tsx.snap +++ b/src/components/__tests__/__snapshots__/Application.tsx.snap @@ -2,7 +2,7 @@ exports[`Application renders correctly 1`] = ` "
- +
diff --git a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap index 84fdca70db..be3615db09 100644 --- a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -62,7 +62,6 @@ exports[`NavigationBar renders correctly with username 1`] = ` Playground
- " `; diff --git a/src/containers/ApplicationContainer.ts b/src/containers/ApplicationContainer.ts index c13c3effed..15a874d8a9 100644 --- a/src/containers/ApplicationContainer.ts +++ b/src/containers/ApplicationContainer.ts @@ -16,6 +16,7 @@ import { IState } from '../reducers/states' const mapStateToProps: MapStateToProps<{ title: string }, {}, IState> = state => ({ title: state.application.title, accessToken: state.session.accessToken, + role: state.session.role, username: state.session.username }) From 77c03d6f82418d9ec3fbc72c1f0cf68e2b7424d6 Mon Sep 17 00:00:00 2001 From: ning Date: Tue, 10 Jul 2018 21:34:28 +0800 Subject: [PATCH 6/7] Add conditional render of grading navlink --- src/components/Application.tsx | 4 +-- src/components/academy/NavigationBar.tsx | 31 +++++++++++++++--------- src/components/academy/index.tsx | 4 ++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/components/Application.tsx b/src/components/Application.tsx index f951163051..4178061b48 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -52,9 +52,9 @@ const Application: React.SFC = props => { * 2. If the user is not logged in, redirect to /login */ const toAcademy = (props: IApplicationProps) => - props.accessToken === undefined + props.accessToken === undefined || props.role === undefined ? () => - : () => + : () => const toLogin = (props: IApplicationProps) => () => ( diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index 3001130adc..4b4c8563fc 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -3,10 +3,17 @@ import { IconNames } from '@blueprintjs/icons' import * as React from 'react' import { NavLink } from 'react-router-dom' +import { Role } from '../../reducers/states' import { assessmentCategoryLink } from '../../utils/paramParseHelpers' import { AssessmentCategories } from '../assessment/assessmentShape' -const NavigationBar: React.SFC<{}> = () => ( +type NavigationBarProps = OwnProps + +type OwnProps = { + role: Role +} + +const NavigationBar: React.SFC = props => ( = () => (
Contests
- - - -
Grading
-
-
+ {props.role === Role.Admin || props.role === Role.Staff ? ( + + + +
Grading
+
+
+ ) : null}
) diff --git a/src/components/academy/index.tsx b/src/components/academy/index.tsx index b9d8ed9cd6..6ec690e380 100644 --- a/src/components/academy/index.tsx +++ b/src/components/academy/index.tsx @@ -5,6 +5,7 @@ import Grading from '../../containers/academy/grading' import AssessmentContainer from '../../containers/assessment' import Game from '../../containers/GameContainer' import { isAcademyRe } from '../../reducers/session' +import { Role } from '../../reducers/states' import { HistoryHelper } from '../../utils/history' import { assessmentCategoryLink } from '../../utils/paramParseHelpers' import { AssessmentCategories, AssessmentCategory } from '../assessment/assessmentShape' @@ -14,6 +15,7 @@ interface IAcademyProps extends IOwnProps, IStateProps, RouteComponentProps<{}> export interface IOwnProps { accessToken?: string + role: Role } export interface IStateProps { @@ -29,7 +31,7 @@ const gradingRegExp = ':submissionId(\\d+)?/:questionId(\\d+)?' export const Academy: React.SFC = props => (
- + Date: Tue, 10 Jul 2018 22:01:25 +0800 Subject: [PATCH 7/7] Add the first tests we've had in 70 years --- src/components/academy/NavigationBar.tsx | 2 +- .../academy/__tests__/NavigationBar.tsx | 32 +++++ .../__snapshots__/NavigationBar.tsx.snap | 110 ++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/components/academy/__tests__/NavigationBar.tsx create mode 100644 src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index 4b4c8563fc..d91f03ced0 100644 --- a/src/components/academy/NavigationBar.tsx +++ b/src/components/academy/NavigationBar.tsx @@ -55,7 +55,7 @@ const NavigationBar: React.SFC = props => ( {props.role === Role.Admin || props.role === Role.Staff ? ( diff --git a/src/components/academy/__tests__/NavigationBar.tsx b/src/components/academy/__tests__/NavigationBar.tsx new file mode 100644 index 0000000000..6d6de8f361 --- /dev/null +++ b/src/components/academy/__tests__/NavigationBar.tsx @@ -0,0 +1,32 @@ +import { shallow } from 'enzyme' +import * as React from 'react' + +import { Role } from '../../../reducers/states' +import NavigationBar from '../NavigationBar' + +test('Grading NavLink does NOT renders for Role.Student', () => { + const props = { role: Role.Student } + const tree = shallow() + expect(tree.debug()).toMatchSnapshot() + expect( + tree.filterWhere(shallowTree => shallowTree.find({ to: 'academy/grading' }).exists()) + ).toHaveLength(0) +}) + +test('Grading NavLink renders for Role.Staff', () => { + const props = { role: Role.Staff } + const tree = shallow() + expect(tree.debug()).toMatchSnapshot() + expect( + tree.filterWhere(shallowTree => shallowTree.find({ to: '/academy/grading' }).exists()) + ).toHaveLength(1) +}) + +test('Grading NavLink renders for Role.Admin', () => { + const props = { role: Role.Admin } + const tree = shallow() + expect(tree.debug()).toMatchSnapshot() + expect( + tree.filterWhere(shallowTree => shallowTree.find({ to: '/academy/grading' }).exists()) + ).toHaveLength(1) +}) diff --git a/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap b/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap new file mode 100644 index 0000000000..6ee9d6815c --- /dev/null +++ b/src/components/academy/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Grading NavLink does NOT renders for Role.Student 1`] = ` +" + + + +
+ Missions +
+
+ + +
+ Sidequests +
+
+ + +
+ Paths +
+
+ + +
+ Contests +
+
+
+
" +`; + +exports[`Grading NavLink renders for Role.Admin 1`] = ` +" + + + +
+ Missions +
+
+ + +
+ Sidequests +
+
+ + +
+ Paths +
+
+ + +
+ Contests +
+
+
+ + + +
+ Grading +
+
+
+
" +`; + +exports[`Grading NavLink renders for Role.Staff 1`] = ` +" + + + +
+ Missions +
+
+ + +
+ Sidequests +
+
+ + +
+ Paths +
+
+ + +
+ Contests +
+
+
+ + + +
+ Grading +
+
+
+
" +`;