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/components/Application.tsx b/src/components/Application.tsx index 19b9b4f44e..4178061b48 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,14 +30,13 @@ const Application: React.SFC = props => { return (
- +
- @@ -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/NavigationBar.tsx b/src/components/NavigationBar.tsx index 1e1806a72e..cdf4b96512 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -1,18 +1,15 @@ -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 + role?: Role } const NavigationBar: React.SFC = props => ( @@ -56,31 +53,13 @@ const NavigationBar: React.SFC = props => (
Playground
- {props.username === undefined ? ( - undefined + {props.username !== undefined && props.role !== undefined ? ( + ) : ( - <> -
- -
-
- -
- - -
{titleCase(props.username)}
-
- + undefined )} ) -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__/Application.tsx.snap b/src/components/__tests__/__snapshots__/Application.tsx.snap index 4a05d50af1..8b1788eb9d 100644 --- a/src/components/__tests__/__snapshots__/Application.tsx.snap +++ b/src/components/__tests__/__snapshots__/Application.tsx.snap @@ -2,14 +2,13 @@ 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..be3615db09 100644 --- a/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavigationBar.tsx.snap @@ -62,20 +62,6 @@ exports[`NavigationBar renders correctly with username 1`] = ` Playground
- -
- -
-
- -
- - -
- Evis Rucer -
-
-
" `; diff --git a/src/components/academy/NavigationBar.tsx b/src/components/academy/NavigationBar.tsx index 3001130adc..d91f03ced0 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/Status.tsx b/src/components/academy/Status.tsx new file mode 100644 index 0000000000..cadda584e9 --- /dev/null +++ b/src/components/academy/Status.tsx @@ -0,0 +1,37 @@ +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 + +type OwnProps = { + username: string + role: Role +} + +const Status: React.SFC = props => ( + <> +
+ +
+
+ +
+ +
+ {controlButton(titleCase(props.username), IconNames.USER)} +
+ +

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

+
+
+ +) + +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/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 +
+
+
+
" +`; 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 => (
- + = state => ({ title: state.application.title, accessToken: state.session.accessToken, + role: state.session.role, username: state.session.username }) 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: {}) { diff --git a/src/styles/_navigationBar.scss b/src/styles/_navigationBar.scss index f86c1ba25c..ecadca6df0 100644 --- a/src/styles/_navigationBar.scss +++ b/src/styles/_navigationBar.scss @@ -33,6 +33,16 @@ .pt-navbar-heading { text-transform: uppercase; } + + .navbar-username { + 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; + } } }