Skip to content

Commit d47cc0e

Browse files
ShenyiCuichownces
andauthored
✨ feat: add total xp table (#2281)
* ✨ feat: add api call to redux and saga * ✨ feat: add new Xp Calculation page * ✨ feat: add nav-link to new page * 🧪 tests: add academy navigation bar test * ✨ feat: add conditional render for xp calculation * 🧪 tests: remove unused snapshot * use React.FC typing for component * shift constants out of react component * memoize callbacks as per PR comment * remove commented out table filter code * shift to useDispatch * update actions to fit naming convention Co-authored-by: En Rong <53928333+chownces@users.noreply.github.com>
1 parent 30b575d commit d47cc0e

File tree

11 files changed

+305
-5
lines changed

11 files changed

+305
-5
lines changed

src/commons/application/ApplicationTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export const defaultSession: SessionState = {
339339
collectibles: {}
340340
},
341341
xp: 0,
342+
allUserXp: undefined,
342343
story: {
343344
story: '',
344345
playStory: false

src/commons/application/actions/SessionActions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
DELETE_ASSESSMENT_CONFIG,
2222
DELETE_USER_COURSE_REGISTRATION,
2323
FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS,
24+
FETCH_ALL_USER_XP,
2425
FETCH_ASSESSMENT,
2526
FETCH_ASSESSMENT_ADMIN,
2627
FETCH_ASSESSMENT_CONFIGS,
@@ -56,6 +57,7 @@ import {
5657
SUBMIT_GRADING_AND_CONTINUE,
5758
Tokens,
5859
UNSUBMIT_SUBMISSION,
60+
UPDATE_ALL_USER_XP,
5961
UPDATE_ASSESSMENT,
6062
UPDATE_ASSESSMENT_CONFIGS,
6163
UPDATE_ASSESSMENT_OVERVIEWS,
@@ -90,6 +92,8 @@ export const fetchTotalXp = () => action(FETCH_TOTAL_XP);
9092

9193
export const fetchTotalXpAdmin = (courseRegId: number) => action(FETCH_TOTAL_XP_ADMIN, courseRegId);
9294

95+
export const fetchAllUserXp = () => action(FETCH_ALL_USER_XP);
96+
9397
export const fetchGrading = (submissionId: number) => action(FETCH_GRADING, submissionId);
9498

9599
/**
@@ -187,6 +191,8 @@ export const updateAssessmentOverviews = (overviews: AssessmentOverview[]) =>
187191

188192
export const updateTotalXp = (totalXp: number) => action(UPDATE_TOTAL_XP, totalXp);
189193

194+
export const updateAllUserXp = (allUserXp: string[][]) => action(UPDATE_ALL_USER_XP, allUserXp);
195+
190196
export const updateAssessment = (assessment: Assessment) => action(UPDATE_ASSESSMENT, assessment);
191197

192198
export const updateGradingOverviews = (overviews: GradingOverview[]) =>

src/commons/application/reducers/SessionsReducer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SET_GOOGLE_USER,
2121
SET_TOKENS,
2222
SET_USER,
23+
UPDATE_ALL_USER_XP,
2324
UPDATE_ASSESSMENT,
2425
UPDATE_ASSESSMENT_OVERVIEWS,
2526
UPDATE_GRADING,
@@ -100,6 +101,8 @@ export const SessionsReducer: Reducer<SessionState> = (
100101
};
101102
case UPDATE_TOTAL_XP:
102103
return { ...state, xp: action.payload };
104+
case UPDATE_ALL_USER_XP:
105+
return { ...state, allUserXp: action.payload };
103106
case UPDATE_GRADING:
104107
const newGradings = new Map(state.gradings);
105108
newGradings.set(action.payload.submissionId, action.payload.grading);

src/commons/application/types/SessionTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const FETCH_COURSE_CONFIG = 'FETCH_COURSE_CONFIG';
1818
export const FETCH_ASSESSMENT = 'FETCH_ASSESSMENT';
1919
export const FETCH_ASSESSMENT_ADMIN = 'FETCH_ASSESSMENT_ADMIN';
2020
export const FETCH_ASSESSMENT_OVERVIEWS = 'FETCH_ASSESSMENT_OVERVIEWS';
21+
export const FETCH_ALL_USER_XP = 'FETCH_ALL_USER_XP';
2122
export const FETCH_TOTAL_XP = 'FETCH_TOTAL_XP';
2223
export const FETCH_TOTAL_XP_ADMIN = 'FETCH_TOTAL_XP_ADMIN';
2324
export const FETCH_GRADING = 'FETCH_GRADING';
@@ -47,6 +48,7 @@ export const REMOVE_GITHUB_OCTOKIT_OBJECT_AND_ACCESS_TOKEN =
4748
export const UNSUBMIT_SUBMISSION = 'UNSUBMIT_SUBMISSION';
4849
export const UPDATE_ASSESSMENT_OVERVIEWS = 'UPDATE_ASSESSMENT_OVERVIEWS';
4950
export const UPDATE_TOTAL_XP = 'UPDATE_TOTAL_XP';
51+
export const UPDATE_ALL_USER_XP = 'UPDATE_ALL_USER_XP';
5052
export const UPDATE_ASSESSMENT = 'UPDATE_ASSESSMENT';
5153
export const UPDATE_GRADING_OVERVIEWS = 'UPDATE_GRADING_OVERVIEWS';
5254
export const UPDATE_GRADING = 'UPDATE_GRADING';
@@ -84,6 +86,7 @@ export type SessionState = {
8486
readonly gameState: GameState;
8587
readonly courseId?: number;
8688
readonly xp: number;
89+
readonly allUserXp: string[][] | undefined;
8790
readonly story: Story;
8891

8992
// Course Configuration

src/commons/navigationBar/subcomponents/AcademyNavigationBar.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ const AcademyNavigationBar: React.FunctionComponent<OwnProps> = props => (
6464
<div className="navbar-button-text hidden-xs hidden-sm hidden-md">Sourcereel</div>
6565
</NavLink>
6666

67+
{props.role === Role.Admin && (
68+
<NavLink
69+
to={`/courses/${props.courseId}/xpcalculation`}
70+
activeClassName={Classes.ACTIVE}
71+
className={classNames('NavigationBar__link', Classes.BUTTON, Classes.MINIMAL)}
72+
>
73+
<Icon icon={IconNames.CALCULATOR} />
74+
<div className="navbar-button-text hidden-xs hidden-sm">XP Calculation</div>
75+
</NavLink>
76+
)}
77+
6778
<NavLink
6879
to={`/courses/${props.courseId}/grading`}
6980
activeClassName={Classes.ACTIVE}

src/commons/navigationBar/subcomponents/__tests__/AcademyNavigationBar.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grad
3030
expect(
3131
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/adminpanel' }).exists())
3232
).toHaveLength(0);
33+
expect(
34+
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/xpcalculation' }).exists())
35+
).toHaveLength(0);
3336
});
3437

3538
test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard and Grading NavLinks render for Role.Staff', () => {
@@ -56,12 +59,15 @@ test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard and G
5659
expect(
5760
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/dashboard' }).exists())
5861
).toHaveLength(1);
62+
expect(
63+
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/xpcalculation' }).exists())
64+
).toHaveLength(0);
5965
expect(
6066
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/adminpanel' }).exists())
6167
).toHaveLength(0);
6268
});
6369

64-
test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grading and AdminPanel NavLinks render for Role.Admin', () => {
70+
test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grading, XP Calculation and AdminPanel NavLinks render for Role.Admin', () => {
6571
const props = {
6672
role: Role.Admin,
6773
notifications: [],
@@ -85,6 +91,9 @@ test('MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grad
8591
expect(
8692
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/dashboard' }).exists())
8793
).toHaveLength(1);
94+
expect(
95+
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/xpcalculation' }).exists())
96+
).toHaveLength(1);
8897
expect(
8998
tree.filterWhere(shallowTree => shallowTree.find({ to: '/courses/0/adminpanel' }).exists())
9099
).toHaveLength(1);

src/commons/navigationBar/subcomponents/__tests__/__snapshots__/AcademyNavigationBar.tsx.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ exports[`MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, G
117117
</Blueprint4.Navbar>"
118118
`;
119119

120-
exports[`MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grading and AdminPanel NavLinks render for Role.Admin 1`] = `
120+
exports[`MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, Grading, XP Calculation and AdminPanel NavLinks render for Role.Admin 1`] = `
121121
"<Blueprint4.Navbar className=\\"NavigationBar secondary-navbar\\">
122122
<Blueprint4.NavbarGroup align=\\"left\\">
123123
<NavLink to=\\"/courses/0/missions\\" activeClassName=\\"bp4-active\\" className=\\"NavigationBar__link bp4-button bp4-minimal\\">
@@ -175,6 +175,12 @@ exports[`MissionControl, GroundControl, Sourcereel, StorySimulator, Dashboard, G
175175
Sourcereel
176176
</div>
177177
</NavLink>
178+
<NavLink to=\\"/courses/0/xpcalculation\\" activeClassName=\\"bp4-active\\" className=\\"NavigationBar__link bp4-button bp4-minimal\\">
179+
<Blueprint4.Icon icon=\\"calculator\\" />
180+
<div className=\\"navbar-button-text hidden-xs hidden-sm\\">
181+
XP Calculation
182+
</div>
183+
</NavLink>
178184
<NavLink to=\\"/courses/0/grading\\" activeClassName=\\"bp4-active\\" className=\\"NavigationBar__link bp4-button bp4-minimal\\">
179185
<Blueprint4.Icon icon=\\"endorsed\\" />
180186
<div className=\\"navbar-button-text hidden-xs hidden-sm hidden-md\\">

src/commons/sagas/BackendSaga.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
DELETE_ASSESSMENT_CONFIG,
4646
DELETE_USER_COURSE_REGISTRATION,
4747
FETCH_ADMIN_PANEL_COURSE_REGISTRATIONS,
48+
FETCH_ALL_USER_XP,
4849
FETCH_ASSESSMENT,
4950
FETCH_ASSESSMENT_ADMIN,
5051
FETCH_ASSESSMENT_CONFIGS,
@@ -78,6 +79,7 @@ import { showSuccessMessage, showWarningMessage } from '../utils/NotificationsHe
7879
import {
7980
deleteAssessment,
8081
deleteSourcecastEntry,
82+
getAllUserXp,
8183
getAssessment,
8284
getAssessmentConfigs,
8385
getAssessmentOverviews,
@@ -237,6 +239,15 @@ function* BackendSaga(): SagaIterator {
237239
}
238240
});
239241

242+
yield takeEvery(FETCH_ALL_USER_XP, function* () {
243+
const tokens: Tokens = yield selectTokens();
244+
245+
const res: { all_users_xp: string[][] } = yield call(getAllUserXp, tokens);
246+
if (res) {
247+
yield put(actions.updateAllUserXp(res.all_users_xp));
248+
}
249+
});
250+
240251
yield takeEvery(FETCH_TOTAL_XP, function* () {
241252
const tokens: Tokens = yield selectTokens();
242253

src/commons/sagas/RequestsSaga.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,21 @@ export const getTotalXp = async (tokens: Tokens, courseRegId?: number): Promise<
491491
return totalXp;
492492
};
493493

494+
/**
495+
* GET /courses/{courseId}/admin/users/total_xp
496+
*/
497+
export const getAllUserXp = async (tokens: Tokens): Promise<number | null> => {
498+
const resp = await request(`${courseId()}/admin/users/total_xp`, 'GET', {
499+
...tokens,
500+
shouldRefresh: true
501+
});
502+
if (!resp || !resp.ok) {
503+
return null; // invalid accessToken _and_ refreshToken
504+
}
505+
const totalXp = await resp.json();
506+
return totalXp;
507+
};
508+
494509
/**
495510
* GET /courses/{courseId}/admin/users/{course_reg_id}/assessments
496511
*/

src/pages/academy/Academy.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Grading from './grading/GradingContainer';
2222
import GroundControl from './groundControl/GroundControlContainer';
2323
import Sourcereel from './sourcereel/SourcereelContainer';
2424
import StorySimulator from './storySimulator/StorySimulator';
25+
import XpCalculation from './xpCalculation/XpCalculation';
2526

2627
const Academy: React.FC<{}> = () => {
2728
const { path, url } = useRouteMatch();
@@ -41,9 +42,10 @@ const Academy: React.FC<{}> = () => {
4142
? [
4243
<Route path={`${path}/groundcontrol`} component={GroundControl} key={0} />,
4344
<Route path={`${path}/grading/${gradingRegExp}`} component={Grading} key={1} />,
44-
<Route path={`${path}/sourcereel`} component={Sourcereel} key={2} />,
45-
<Route path={`${path}/storysimulator`} component={StorySimulator} key={3} />,
46-
<Route path={`${path}/dashboard`} component={DashboardContainer} key={4} />
45+
<Route path={`${path}/xpcalculation`} component={XpCalculation} key={2} />,
46+
<Route path={`${path}/sourcereel`} component={Sourcereel} key={3} />,
47+
<Route path={`${path}/storysimulator`} component={StorySimulator} key={4} />,
48+
<Route path={`${path}/dashboard`} component={DashboardContainer} key={5} />
4749
]
4850
: null;
4951
return (

0 commit comments

Comments
 (0)