From ca978ba57e9a0047d92e9450c2ab78e11157b967 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 8 Aug 2020 18:53:55 +0800 Subject: [PATCH 001/143] Move goal type to meta --- src/commons/mocks/AchievementMocks.ts | 50 +++++++++++++------- src/features/achievement/AchievementTypes.ts | 8 ++-- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index e09f582ca7..13193fbafa 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -231,8 +231,8 @@ export const mockGoals: AchievementGoal[] = [ id: 0, text: 'Bonus for completing Colorful Carpet & Beyond the Second Dimension Achievements', maxExp: 100, - type: GoalType.BINARY, meta: { + type: GoalType.BINARY, condition: `AND( { event: 'achievement', @@ -251,8 +251,11 @@ export const mockGoals: AchievementGoal[] = [ id: 1, text: 'XP earned from Beyond the Second Dimension Mission', maxExp: 250, - type: GoalType.ASSESSMENT, - meta: { assessmentId: 5, requiredCompletionExp: 200 }, + meta: { + type: GoalType.ASSESSMENT, + assessmentId: '5', + requiredCompletionExp: 200 + }, exp: 213, completed: true }, @@ -260,8 +263,11 @@ export const mockGoals: AchievementGoal[] = [ id: 2, text: 'XP earned from Colorful Carpet Mission', maxExp: 250, - type: GoalType.ASSESSMENT, - meta: { assessmentId: 3, requiredCompletionExp: 200 }, + meta: { + type: GoalType.ASSESSMENT, + assessmentId: '3', + requiredCompletionExp: 200 + }, exp: 0, completed: false }, @@ -269,8 +275,8 @@ export const mockGoals: AchievementGoal[] = [ id: 3, text: 'Bonus for completing Curve Introduction & Curve Manipulation Achievements', maxExp: 100, - type: GoalType.BINARY, meta: { + type: GoalType.BINARY, condition: `AND( { event: 'achievement', @@ -289,8 +295,11 @@ export const mockGoals: AchievementGoal[] = [ id: 4, text: 'XP earned from Curve Introduction Mission', maxExp: 250, - type: GoalType.ASSESSMENT, - meta: { assessmentId: 7, requiredCompletionExp: 150 }, + meta: { + type: GoalType.ASSESSMENT, + assessmentId: '7', + requiredCompletionExp: 150 + }, exp: 178, completed: true }, @@ -298,8 +307,11 @@ export const mockGoals: AchievementGoal[] = [ id: 5, text: 'XP earned from Curve Manipulation Mission', maxExp: 250, - type: GoalType.ASSESSMENT, - meta: { assessmentId: 8, requiredCompletionExp: 200 }, + meta: { + type: GoalType.ASSESSMENT, + assessmentId: '8', + requiredCompletionExp: 200 + }, exp: 191, completed: false }, @@ -307,8 +319,8 @@ export const mockGoals: AchievementGoal[] = [ id: 6, text: 'Submit Source 3 Path', maxExp: 100, - type: GoalType.BINARY, meta: { + type: GoalType.BINARY, condition: ` { event: 'assessment-submission', @@ -323,9 +335,9 @@ export const mockGoals: AchievementGoal[] = [ id: 7, text: 'XP earned from Source 3 Path', maxExp: 300, - type: GoalType.ASSESSMENT, meta: { - assessmentId: 12, + type: GoalType.ASSESSMENT, + assessmentId: '12', requiredCompletionExp: 300 }, exp: 300, @@ -335,8 +347,9 @@ export const mockGoals: AchievementGoal[] = [ id: 8, text: 'Each Top Voted answer in Piazza gives 10 XP', maxExp: 100, - type: GoalType.MANUAL, - meta: {}, + meta: { + type: GoalType.MANUAL + }, exp: 40, completed: false }, @@ -344,8 +357,9 @@ export const mockGoals: AchievementGoal[] = [ id: 9, text: 'Submit 1 PR to Source Academy Github', maxExp: 100, - type: GoalType.MANUAL, - meta: {}, + meta: { + type: GoalType.MANUAL + }, exp: 100, completed: true }, @@ -353,8 +367,8 @@ export const mockGoals: AchievementGoal[] = [ id: 10, text: 'Be the Koolest Kidz in SOC by redeeming this 100 XP achievement yourself', maxExp: 100, - type: GoalType.BINARY, meta: { + type: GoalType.BINARY, condition: ` { event: 'achievement', diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index c172a1351f..0af3aadb11 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -67,14 +67,12 @@ export type AchievementGoal = GoalDefinition & GoalProgress; * @param {number} id unique id of the goal * @param {string} text goal description * @param {number} maxExp maximum attainable exp of the goal - * @param {GoalType} type type of goal, string enum * @param {GoalMeta} meta contains meta data relevant to the goal type */ export type GoalDefinition = { id: number; text: string; maxExp: number; - type: GoalType; meta: GoalMeta; }; @@ -101,16 +99,18 @@ export enum GoalType { export type GoalMeta = AssessmentMeta | BinaryMeta | ManualMeta; export type AssessmentMeta = { - assessmentId: number; + type: GoalType; + assessmentId: string; // e.g. 'M1A', 'P2' requiredCompletionExp: number; }; export type BinaryMeta = { + type: GoalType; condition: string; }; export type ManualMeta = { - // currently nothing + type: GoalType; }; /** From dc8e421a5cb3a3ef87fa237d909d6867e2dacd2c Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 8 Aug 2020 19:30:06 +0800 Subject: [PATCH 002/143] Add dashboard useEffect trigger state --- .../subcomponents/AchievementDashboard.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 55c680451f..b4f9f6c879 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -46,15 +46,19 @@ export const generateAchievementTasks = ( function Dashboard(props: DispatchProps & StateProps) { const { inferencer, name, group, handleGetAchievements } = props; + const filterState = useState(FilterStatus.ALL); + const [filterStatus] = filterState; + + /** + * The dashboard fetches the latest achievements and goals from backend + * when the page is mounted or the filter status is changed + */ useEffect(() => { if (Constants.useBackend) { handleGetAchievements(); // TODO: handleGetGoals(); } - }, [handleGetAchievements]); - - const filterState = useState(FilterStatus.ALL); - const [filterStatus] = filterState; + }, [filterStatus, handleGetAchievements]); // If an achievement is focused, the cards glow and dashboard displays the AchievementView const focusState = useState(-1); From 395aba57e092ee186ab151763e26d60cd471c4dc Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 8 Aug 2020 19:49:31 +0800 Subject: [PATCH 003/143] Add handleGetOwnGoal in dashboard --- .../subcomponents/AchievementDashboard.tsx | 16 ++++++++-------- .../AchievementDashboardContainer.ts | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index b4f9f6c879..e5811b9444 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -11,8 +11,8 @@ import { FilterStatus } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { handleGetAchievements: () => void; + handleGetOwnGoals: () => void; // TODO: handleGetGoals: () => void; - // TODO: handleGetOwnGoals: () => void; }; export type StateProps = { @@ -44,21 +44,21 @@ export const generateAchievementTasks = ( }; function Dashboard(props: DispatchProps & StateProps) { - const { inferencer, name, group, handleGetAchievements } = props; - - const filterState = useState(FilterStatus.ALL); - const [filterStatus] = filterState; + const { inferencer, name, group, handleGetAchievements, handleGetOwnGoals } = props; /** * The dashboard fetches the latest achievements and goals from backend - * when the page is mounted or the filter status is changed + * when the page is rendered */ useEffect(() => { if (Constants.useBackend) { handleGetAchievements(); - // TODO: handleGetGoals(); + handleGetOwnGoals(); } - }, [filterStatus, handleGetAchievements]); + }, [handleGetAchievements, handleGetOwnGoals]); + + const filterState = useState(FilterStatus.ALL); + const [filterStatus] = filterState; // If an achievement is focused, the cards glow and dashboard displays the AchievementView const focusState = useState(-1); diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 3ad836aa2d..0f7b9c498f 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -5,7 +5,7 @@ import AchievementInferencer from '../../../commons/achievement/utils/Achievemen import { OverallState } from '../../../commons/application/ApplicationTypes'; import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import Constants from '../../../commons/utils/Constants'; -import { getAchievements } from '../../../features/achievement/AchievementActions'; +import { getAchievements, getOwnGoals } from '../../../features/achievement/AchievementActions'; import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; const mapStateToProps: MapStateToProps = state => ({ @@ -19,9 +19,9 @@ const mapStateToProps: MapStateToProps = state => const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { - handleGetAchievements: getAchievements + handleGetAchievements: getAchievements, + handleGetOwnGoals: getOwnGoals // TODO: handleGetGoals: getGoals - // TODO: handleGetOwnGoals: getOwnGoals }, dispatch ); From 587fdb931962cc94eaa4a215cd4f047a80eaa62c Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 8 Aug 2020 21:06:36 +0800 Subject: [PATCH 004/143] Add bulkUpdates --- src/commons/sagas/AchievementSaga.ts | 38 +++++++++++++++ src/commons/sagas/RequestsSaga.ts | 38 +++++++++++++++ .../achievement/AchievementActions.ts | 7 +++ src/features/achievement/AchievementTypes.ts | 2 + .../control/AchievementControl.tsx | 48 ++++++------------- .../control/AchievementControlContainer.ts | 20 +++----- 6 files changed, 106 insertions(+), 47 deletions(-) diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index 0505772e76..baf844d769 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -2,6 +2,8 @@ import { SagaIterator } from 'redux-saga'; import { call, put, select } from 'redux-saga/effects'; import { + BULK_UPDATE_ACHIEVEMENTS, + BULK_UPDATE_GOALS, EDIT_ACHIEVEMENT, EDIT_GOAL, GET_ACHIEVEMENTS, @@ -14,6 +16,8 @@ import { import { OverallState } from '../application/ApplicationTypes'; import { actions } from '../utils/ActionsHelper'; import { + bulkUpdateAchievements, + bulkUpdateGoals, editAchievement, editGoal, getAchievements, @@ -26,6 +30,40 @@ import { import { safeTakeEvery as takeEvery } from './SafeEffects'; export default function* AchievementSaga(): SagaIterator { + yield takeEvery(BULK_UPDATE_ACHIEVEMENTS, function* ( + action: ReturnType + ) { + const tokens = yield select((state: OverallState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + + const achievements = action.payload; + + const resp = yield call(bulkUpdateAchievements, achievements, tokens); + + if (!resp) { + return; + } + }); + + yield takeEvery(BULK_UPDATE_GOALS, function* ( + action: ReturnType + ) { + const tokens = yield select((state: OverallState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + + const goals = action.payload; + + const resp = yield call(bulkUpdateGoals, goals, tokens); + + if (!resp) { + return; + } + }); + yield takeEvery(EDIT_ACHIEVEMENT, function* (action: ReturnType) { const tokens = yield select((state: OverallState) => ({ accessToken: state.session.accessToken, diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 1fe4354056..3c406a5f4d 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -202,6 +202,44 @@ export async function getOwnGoals(tokens: Tokens): Promise { + const resp = await request(`admin/achievements`, 'POST', { + accessToken: tokens.accessToken, + body: { achievements: achievements }, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + + return resp; +} + +/** + * PUT /admin/goals + */ +export async function bulkUpdateGoals( + goals: GoalDefinition[], + tokens: Tokens +): Promise { + const resp = await request(`admin/goals`, 'POST', { + accessToken: tokens.accessToken, + body: { goals: goals }, + noHeaderAccept: true, + refreshToken: tokens.refreshToken, + shouldAutoLogout: false, + shouldRefresh: true + }); + + return resp; +} + /** * POST /achievements/:achievement_id */ diff --git a/src/features/achievement/AchievementActions.ts b/src/features/achievement/AchievementActions.ts index df12dc4892..4b9edc9d40 100644 --- a/src/features/achievement/AchievementActions.ts +++ b/src/features/achievement/AchievementActions.ts @@ -3,6 +3,8 @@ import { action } from 'typesafe-actions'; import { AchievementGoal, AchievementItem, + BULK_UPDATE_ACHIEVEMENTS, + BULK_UPDATE_GOALS, EDIT_ACHIEVEMENT, EDIT_GOAL, GET_ACHIEVEMENTS, @@ -17,6 +19,11 @@ import { UPDATE_GOAL_PROGRESS } from './AchievementTypes'; +export const bulkUpdateAchievements = (achievements: AchievementItem[]) => + action(BULK_UPDATE_ACHIEVEMENTS, achievements); + +export const bulkUpdateGoals = (goals: GoalDefinition[]) => action(BULK_UPDATE_GOALS, goals); + export const editAchievement = (achievement: AchievementItem) => action(EDIT_ACHIEVEMENT, achievement); diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 0af3aadb11..394e57839b 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -1,3 +1,5 @@ +export const BULK_UPDATE_ACHIEVEMENTS = 'BULK_UPDATE_ACHIEVEMENTS'; +export const BULK_UPDATE_GOALS = 'BULK_UPDATE_GOALS'; export const EDIT_ACHIEVEMENT = 'EDIT_ACHIEVEMENT'; export const EDIT_GOAL = 'EDIT_GOAL'; export const GET_ACHIEVEMENTS = 'GET_ACHIEVEMENTS'; diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 9ed0a142e8..631e407574 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -1,17 +1,17 @@ +import { noop } from 'lodash'; import React, { useEffect, useState } from 'react'; -import AchievementPreview from 'src/commons/achievement/control/AchievementPreview'; import AchievementEditor from '../../../commons/achievement/control/AchievementEditor'; +import AchievementPreview from '../../../commons/achievement/control/AchievementPreview'; import GoalEditor from '../../../commons/achievement/control/GoalEditor'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; -import { AchievementGoal, AchievementItem } from '../../../features/achievement/AchievementTypes'; +import { AchievementItem, GoalDefinition } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { - handleEditAchievement: (achievement: AchievementItem) => void; + handleBulkUpdateAchievements: (achievements: AchievementItem[]) => void; + handleBulkUpdateGoals: (goals: GoalDefinition[]) => void; handleGetAchievements: () => void; - handleRemoveAchievement: (achievement: AchievementItem) => void; - handleRemoveGoal: (goal: AchievementGoal, achievement: AchievementItem) => void; - handleSaveAchievements: (achievements: AchievementItem[]) => void; + handleGetOwnGoals: () => void; }; export type StateProps = { @@ -19,15 +19,9 @@ export type StateProps = { }; function AchievementControl(props: DispatchProps & StateProps) { - const { - inferencer, - handleEditAchievement, - handleGetAchievements, - handleRemoveAchievement, - handleRemoveGoal - } = props; + const { inferencer, handleGetAchievements, handleGetOwnGoals } = props; - const [editorUnsavedChanges, setEditorUnsavedChanges] = useState(0); + const [editorUnsavedChanges] = useState(0); const [panelPendingUpload] = useState(false); useEffect(() => { @@ -37,19 +31,7 @@ function AchievementControl(props: DispatchProps & StateProps) { handleGetAchievements(); window.onbeforeunload = null; } - }, [handleGetAchievements, editorUnsavedChanges, panelPendingUpload]); - - const addUnsavedChanges = (changes: number) => - setEditorUnsavedChanges(editorUnsavedChanges + changes); - - const addUnsavedChange = () => addUnsavedChanges(1); - const removeUnsavedChange = () => addUnsavedChanges(-1); - - const updateAchievements = () => { - for (const achievement of inferencer.getAchievements()) { - handleEditAchievement(achievement); - } - }; + }, [editorUnsavedChanges, panelPendingUpload, handleGetAchievements, handleGetOwnGoals]); const [render, setRender] = useState(); const forceRender = () => setRender(!render); @@ -60,13 +42,13 @@ function AchievementControl(props: DispatchProps & StateProps) { diff --git a/src/pages/achievement/control/AchievementControlContainer.ts b/src/pages/achievement/control/AchievementControlContainer.ts index 79163f68a0..c17b2572de 100644 --- a/src/pages/achievement/control/AchievementControlContainer.ts +++ b/src/pages/achievement/control/AchievementControlContainer.ts @@ -6,14 +6,10 @@ import AchievementInferencer from '../../../commons/achievement/utils/Achievemen import { OverallState } from '../../../commons/application/ApplicationTypes'; import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import { - editAchievement, - editGoal, + bulkUpdateAchievements, + bulkUpdateGoals, getAchievements, - getOwnGoals, - removeAchievement, - removeGoal, - saveAchievements, - saveGoals + getOwnGoals } from '../../../features/achievement/AchievementActions'; import AchievementControl, { DispatchProps, StateProps } from './AchievementControl'; @@ -26,14 +22,10 @@ const mapStateToProps: MapStateToProps = state => const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { - handleEditAchievement: editAchievement, - handleEditGoal: editGoal, + handleBulkUpdateAchievements: bulkUpdateAchievements, + handleBulkUpdateGoals: bulkUpdateGoals, handleGetAchievements: getAchievements, - handleGetOwnGoals: getOwnGoals, - handleRemoveAchievement: removeAchievement, - handleRemoveGoal: removeGoal, - handleSaveAchievements: saveAchievements, - handleSaveGoals: saveGoals + handleGetOwnGoals: getOwnGoals }, dispatch ); From 71959ab9d0f716a978d5d3a23e4ff66c21aa2353 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 9 Aug 2020 00:53:40 +0800 Subject: [PATCH 005/143] Initial refactor editor --- .../achievement/control/AchievementEditor.tsx | 46 ++---- .../control/AchievementPreview.tsx | 42 +++-- .../editorTools/AchievementTemplate.tsx | 2 +- .../editorTools/EditableAchievementCard.tsx | 143 +++++++----------- .../editableUtils/AchievementAdder.tsx | 13 +- .../editableUtils/AchievementUploader.tsx | 10 +- .../achievement/utils/AchievementCard.tsx | 2 +- .../control/AchievementControl.tsx | 46 +++--- src/styles/_achievementcontrol.scss | 12 +- 9 files changed, 146 insertions(+), 170 deletions(-) diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index d9d084e23d..32fb5f5dec 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -6,26 +6,12 @@ import AchievementAdder from './editorTools/editableUtils/AchievementAdder'; type AchievementEditorProps = { inferencer: AchievementInferencer; - updateAchievements: any; - editAchievement: any; - forceRender: any; - addUnsavedChange: any; - removeUnsavedChange: any; - removeGoal: any; - removeAchievement: any; + publishState: [boolean, any]; + forceRender: () => void; }; function AchievementEditor(props: AchievementEditorProps) { - const { - inferencer, - updateAchievements, - editAchievement, - forceRender, - addUnsavedChange, - removeUnsavedChange, - removeGoal, - removeAchievement - } = props; + const { inferencer, publishState, forceRender } = props; /** * NOTE: This helps us to ensure that only ONE achievement is added @@ -39,34 +25,28 @@ function AchievementEditor(props: AchievementEditorProps) { * is being added to the systen and the admin is not allowed to add two achievements * at one go. */ - const [adderId, setAdderId] = useState(-1); + const controlState = useState(-1); - const mapAchievementIdsToEditableCard = (achievementIds: number[]) => - achievementIds.map(id => ( + const generateEditableCards = (inferencer: AchievementInferencer) => { + const achievementIds = inferencer.listIds().reverse(); + return achievementIds.map(id => ( )); + }; return (
- +
-
    - {mapAchievementIdsToEditableCard(inferencer.listIds())} -
+
    {generateEditableCards(inferencer)}
); } diff --git a/src/commons/achievement/control/AchievementPreview.tsx b/src/commons/achievement/control/AchievementPreview.tsx index 5e589120ac..af320b10bf 100644 --- a/src/commons/achievement/control/AchievementPreview.tsx +++ b/src/commons/achievement/control/AchievementPreview.tsx @@ -1,7 +1,7 @@ import { Button, Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; -import { FilterStatus } from 'src/features/achievement/AchievementTypes'; +import { AchievementItem, FilterStatus } from 'src/features/achievement/AchievementTypes'; import { generateAchievementTasks } from 'src/pages/achievement/subcomponents/AchievementDashboard'; import AchievementView from '../AchievementView'; @@ -9,10 +9,23 @@ import AchievementInferencer from '../utils/AchievementInferencer'; type AchievementPreviewProps = { inferencer: AchievementInferencer; + publishState: [boolean, any]; + publishAchievements: (achievements: AchievementItem[]) => void; + // TODO: publishGoals: (goals: GoalDefinition[]) => void; }; function AchievementPreview(props: AchievementPreviewProps) { - const { inferencer } = props; + const { inferencer, publishState, publishAchievements } = props; + + const achievements = inferencer.getAchievements(); + // TODO: const goals = inferencer.getGoalDefinitions(); + + const [canPublish, setCanPublish] = publishState; + const handlePublish = () => { + // TODO: publishGoals(goals); + publishAchievements(achievements); + setCanPublish(false); + }; const [viewMode, setViewMode] = useState(false); const toggleMode = () => setViewMode(!viewMode); @@ -23,13 +36,24 @@ function AchievementPreview(props: AchievementPreviewProps) { return (
-
{viewMode ? (
{focusId < 0 ? ( diff --git a/src/commons/achievement/control/editorTools/AchievementTemplate.tsx b/src/commons/achievement/control/editorTools/AchievementTemplate.tsx index 5665208844..4e9f3fcdcb 100644 --- a/src/commons/achievement/control/editorTools/AchievementTemplate.tsx +++ b/src/commons/achievement/control/editorTools/AchievementTemplate.tsx @@ -13,7 +13,7 @@ export const viewTemplate: AchievementView = { export const achievementTemplate: AchievementItem = { id: 0, - title: '', + title: 'Achievement Title Here', ability: AchievementAbility.CORE, isTask: false, position: 0, diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx index b23c858a11..f0a414cc40 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx @@ -16,116 +16,83 @@ import EditableAchievementTitle from './editableUtils/EditableAchievementTitle'; import EditableAchievementView from './editableView/EditableAchievementView'; type EditableAchievementCardProps = { - achievement: AchievementItem; + id: number; inferencer: AchievementInferencer; - updateAchievements: any; - editAchievement: any; - forceRender: any; - adderId: number; - setAdderId: any; - addUnsavedChange: any; - removeUnsavedChange: any; - removeGoal: any; - removeAchievement: any; + controlState: [number, any]; + publishState: [boolean, any]; + forceRender: () => void; }; function EditableAchievementCard(props: EditableAchievementCardProps) { - const { - achievement, - inferencer, - updateAchievements, - editAchievement, - adderId, - setAdderId, - addUnsavedChange, - removeUnsavedChange, - removeAchievement - } = props; - - const [editableAchievement, setEditableAchievement] = useState(achievement); - const { id, title, ability, deadline, cardTileUrl, release, view } = editableAchievement; + const { id, inferencer, controlState, publishState, forceRender } = props; - const [hasChanges, setHasChanges] = useState(false); - const [pendingUpload, setPendingUpload] = useState(false); - - const setUnsaved = () => { - if (!hasChanges) { - setHasChanges(true); - addUnsavedChange(); - } - }; + const [controlId, setControlId] = controlState; + const [, setCanPublish] = publishState; - const setUpload = () => { - setPendingUpload(false); - removeUnsavedChange(); - }; + const achievement = inferencer.getAchievementItem(id); + const [editableAchievement, setEditableAchievement] = useState(achievement); + const { + title, + ability, + deadline, + release, + /* TODO: + isTask, + position, + prerequisiteIds, + goalIds, */ + cardTileUrl, + view + } = editableAchievement; + + const [isDirty, setIsDirty] = useState(false); const handleSaveChanges = () => { inferencer.modifyAchievement(editableAchievement); - setHasChanges(false); - setPendingUpload(true); - - // It means that there is currently an item being added. - // Once this item is added, we can reset it to -1, which means - // that there is no achievement being added. - // indicating to the system that there is no achievement - // being added. - if (id === adderId) { - setAdderId(-1); + setIsDirty(false); + setCanPublish(true); + forceRender(); + /** + * It means that there is currently an item being added. + * Once this item is added, we can reset it to -1, which means + * that there is no achievement being added. + * indicating to the system that there is no achievement + * being added. + */ + if (id === controlId) { + setControlId(-1); } }; - const handleUploadChanges = () => { - editAchievement(editableAchievement); - setUpload(); + const handleDiscardChanges = () => { + setEditableAchievement(achievement); + setIsDirty(false); }; const handleDeleteAchievement = () => { inferencer.removeAchievement(id); + setCanPublish(true); + forceRender(); - removeAchievement(editableAchievement); - updateAchievements(); - - if (id === adderId) { - setAdderId(-1); + if (id === controlId) { + setControlId(-1); } }; - const handleDiscardChanges = () => { - setEditableAchievement(achievement); - setHasChanges(false); - setUpload(); - }; - const handleChangeTitle = (title: string) => { setEditableAchievement({ ...editableAchievement, title: title }); - setUnsaved(); - }; - - /* - const handleEditGoals = (goals: AchievementGoal[], shouldUpdate: boolean) => { - setEditableAchievement({ - ...editableAchievement, - goalIds: goalIds - }); - - inferencer.modifyAchievement(editableAchievement); - editAchievement(editableAchievement); + setIsDirty(true); }; - const handleRemoveGoal = (goal: AchievementGoal) => { - removeGoal(goal, editableAchievement); - }; - */ const handleChangeBackground = (cardTileUrl: string) => { setEditableAchievement({ ...editableAchievement, cardTileUrl: cardTileUrl }); - setUnsaved(); + setIsDirty(true); }; const handleChangeRelease = (release: Date) => { @@ -133,7 +100,7 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { ...editableAchievement, release: release }); - setUnsaved(); + setIsDirty(true); }; const handleChangeDeadline = (deadline: Date) => { @@ -141,15 +108,15 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { ...editableAchievement, deadline: deadline }); - setUnsaved(); + setIsDirty(true); }; - const handleChangeAbility = (ability: AchievementAbility, e: any) => { + const handleChangeAbility = (ability: AchievementAbility) => { setEditableAchievement({ ...editableAchievement, ability: ability }); - setUnsaved(); + setIsDirty(true); }; const handleChangeView = (view: AchievementView) => { @@ -157,7 +124,7 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { ...editableAchievement, view: view }); - setUnsaved(); + setIsDirty(true); }; return ( @@ -171,11 +138,9 @@ function EditableAchievementCard(props: EditableAchievementCardProps) {
@@ -184,12 +149,6 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { cardTileUrl={cardTileUrl} setcardTileUrl={handleChangeBackground} /> - {/* TODO: Implement goal editor - */}
diff --git a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx b/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx index 61f3450ee9..aad15be1a4 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx +++ b/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx @@ -6,19 +6,20 @@ import { achievementTemplate } from '../AchievementTemplate'; type AchievementAdderProps = { inferencer: AchievementInferencer; - adderId: number; - setAdderId: any; + controlState: [number, any]; }; function AchievementAdder(props: AchievementAdderProps) { - const { inferencer, adderId, setAdderId } = props; + const { inferencer, controlState } = props; + + const [controlId, setControlId] = controlState; const handleAddAchievement = () => { - const newId = inferencer.insertAchievement(achievementTemplate); - setAdderId(newId); + const createdId = inferencer.insertAchievement(achievementTemplate); + setControlId(createdId); }; - const disableAdder = adderId !== -1; + const disableAdder = controlId !== -1; return ( - - -
-
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Rune Master - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Beyond the Second Dimension - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Colorful Carpet - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Enter your title here - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Curve Wizard - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Curve Introduction - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Curve Manipulation - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - The Source-rer - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Power of Friendship - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Piazza Guru - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - That's the Spirit - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    - - - -
    - - - -
    -
    - -
    -
    - -
    -
    - - - -
    - - - -
    -
    -
    - -
    -

    - -
    - - Kool Kidz - -
    -
    -

    -
    -
    -
    - -
    - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - -
    -
    -
    -
    -
    -
- -
" -`; diff --git a/src/commons/achievement/control/editorTools/__tests__/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/__tests__/EditableAchievementCard.tsx deleted file mode 100644 index 28058660be..0000000000 --- a/src/commons/achievement/control/editorTools/__tests__/EditableAchievementCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockAchievements, mockGoals } from '../../../../mocks/AchievementMocks'; -import AchievementInferencer from '../../../utils/AchievementInferencer'; -import EditableAchievementCard from '../EditableAchievementCard'; - -const mockProps = { - achievement: mockAchievements[0], - inferencer: new AchievementInferencer(mockAchievements, mockGoals), - updateAchievements: () => {}, - editAchievement: () => {}, - forceRender: () => {}, - adderId: mockAchievements[0].id, - setAdderId: () => {}, - addUnsavedChange: () => {}, - removeUnsavedChange: () => {}, - removeGoal: () => {}, - removeAchievement: () => {} -}; - -test('EditableAchievementCard component renders correctly', () => { - const component = ; - const tree = mount(component); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/__tests__/__snapshots__/EditableAchievementCard.tsx.snap b/src/commons/achievement/control/editorTools/__tests__/__snapshots__/EditableAchievementCard.tsx.snap deleted file mode 100644 index 6e1f06ee52..0000000000 --- a/src/commons/achievement/control/editorTools/__tests__/__snapshots__/EditableAchievementCard.tsx.snap +++ /dev/null @@ -1,199 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementCard component renders correctly 1`] = ` -" - -
-
- -
-
- - - -
- - - -
-
- -
-
- -
-
- - - -
- - - -
-
-
- -
-

- -
- - Rune Master - -
-
-

-
-
-
- -
- - - - - - - - - -
-
- - - -
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - - -
-
- -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - - -
-
-
-
-
- - - - - - - - -
-
-
-
-
" -`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementAdder.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementAdder.tsx deleted file mode 100644 index 5b931860e0..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementAdder.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockAchievements, mockGoals } from '../../../../../mocks/AchievementMocks'; -import AchievementInferencer from '../../../../utils/AchievementInferencer'; -import AchievementAdder from '../AchievementAdder'; - -const mockProps = { - inferencer: new AchievementInferencer(mockAchievements, mockGoals), - adderId: 0, - setAdderId: () => {} -}; - -test('AchievementAdder component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementAdder.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementAdder.tsx.snap deleted file mode 100644 index b6281f993c..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementAdder.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AchievementAdder component renders correctly 1`] = ` -" - - - -" -`; From fcdce94b1be3acf6eba96c082fcb79e17dca7ecc Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 9 Aug 2020 16:29:07 +0800 Subject: [PATCH 007/143] Add bulkUpdateGoals --- .../achievement/control/AchievementEditor.tsx | 6 +-- .../control/AchievementPreview.tsx | 18 +++++--- .../achievement/control/ControlPanel.tsx | 2 +- .../editorTools/EditableAchievementCard.tsx | 4 +- .../utils/AchievementInferencer.ts | 43 +++++++++++++++---- .../utils/__tests__/Inferencer.test.ts | 4 +- .../control/AchievementControl.tsx | 16 ++++--- 7 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index 32fb5f5dec..0bc1ceb98a 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -6,12 +6,12 @@ import AchievementAdder from './editorTools/editableUtils/AchievementAdder'; type AchievementEditorProps = { inferencer: AchievementInferencer; - publishState: [boolean, any]; forceRender: () => void; + publishState: [boolean, any]; }; function AchievementEditor(props: AchievementEditorProps) { - const { inferencer, publishState, forceRender } = props; + const { inferencer, forceRender, publishState } = props; /** * NOTE: This helps us to ensure that only ONE achievement is added @@ -35,8 +35,8 @@ function AchievementEditor(props: AchievementEditorProps) { id={id} inferencer={inferencer} controlState={controlState} - publishState={publishState} forceRender={forceRender} + publishState={publishState} /> )); }; diff --git a/src/commons/achievement/control/AchievementPreview.tsx b/src/commons/achievement/control/AchievementPreview.tsx index af320b10bf..73420be828 100644 --- a/src/commons/achievement/control/AchievementPreview.tsx +++ b/src/commons/achievement/control/AchievementPreview.tsx @@ -1,7 +1,11 @@ import { Button, Icon } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; -import { AchievementItem, FilterStatus } from 'src/features/achievement/AchievementTypes'; +import { + AchievementItem, + FilterStatus, + GoalDefinition +} from 'src/features/achievement/AchievementTypes'; import { generateAchievementTasks } from 'src/pages/achievement/subcomponents/AchievementDashboard'; import AchievementView from '../AchievementView'; @@ -9,20 +13,20 @@ import AchievementInferencer from '../utils/AchievementInferencer'; type AchievementPreviewProps = { inferencer: AchievementInferencer; - publishState: [boolean, any]; publishAchievements: (achievements: AchievementItem[]) => void; - // TODO: publishGoals: (goals: GoalDefinition[]) => void; + publishGoals: (goals: GoalDefinition[]) => void; + publishState: [boolean, any]; }; function AchievementPreview(props: AchievementPreviewProps) { - const { inferencer, publishState, publishAchievements } = props; + const { inferencer, publishAchievements, publishGoals, publishState } = props; - const achievements = inferencer.getAchievements(); - // TODO: const goals = inferencer.getGoalDefinitions(); + const achievements = inferencer.getAllAchievement(); + const goals = inferencer.getAllGoalDefinition(); const [canPublish, setCanPublish] = publishState; const handlePublish = () => { - // TODO: publishGoals(goals); + publishGoals(goals); publishAchievements(achievements); setCanPublish(false); }; diff --git a/src/commons/achievement/control/ControlPanel.tsx b/src/commons/achievement/control/ControlPanel.tsx index ffc5307bf5..866f73f2cf 100644 --- a/src/commons/achievement/control/ControlPanel.tsx +++ b/src/commons/achievement/control/ControlPanel.tsx @@ -27,7 +27,7 @@ function ControlPanel(props: ControlPanelProps) { const handleSaveChanges = () => { setPendingUpload(true); - saveAchievementsToFrontEnd(inferencer.getAchievements()); + saveAchievementsToFrontEnd(inferencer.getAllAchievement()); forceRender(); }; diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx index f0a414cc40..2a61a1faf1 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx @@ -19,12 +19,12 @@ type EditableAchievementCardProps = { id: number; inferencer: AchievementInferencer; controlState: [number, any]; - publishState: [boolean, any]; forceRender: () => void; + publishState: [boolean, any]; }; function EditableAchievementCard(props: EditableAchievementCardProps) { - const { id, inferencer, controlState, publishState, forceRender } = props; + const { id, inferencer, controlState, forceRender, publishState } = props; const [controlId, setControlId] = controlState; const [, setCanPublish] = publishState; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index cbc21debae..db18233b0f 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -3,7 +3,8 @@ import assert from 'assert'; import { AchievementGoal, AchievementItem, - AchievementStatus + AchievementStatus, + GoalDefinition } from '../../../features/achievement/AchievementTypes'; import { isExpired } from './DateHelper'; @@ -67,10 +68,36 @@ class AchievementInferencer { /** * Returns an array of AchievementItem */ - public getAchievements() { + public getAllAchievement() { return [...this.nodeList.values()].map(node => node.achievement); } + /** + * Returns an array of AchievementGoal + */ + public getAllGoal() { + return [...this.goalList.values()]; + } + + /** + * Returns an array of GoalDefinition + */ + public getAllGoalDefinition() { + /** + * NOTE: This is a very artificial way of instantiating a GoalDefinition + * from AchievementGoal due to the limitation of type alias. Ideally, + * GoalDefinition, GoalProgress should be declared as classes. + * + * Read: https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c + * + * TODO: Revisit AchievementGoal, GoalDefinition, GoalProgress declarations + */ + return this.getAllGoal().map(goal => { + const { id, text, maxExp, meta } = goal; + return { id, text, maxExp, meta } as GoalDefinition; + }); + } + /** * Returns the AchievementItem * @@ -157,7 +184,7 @@ class AchievementInferencer { * Returns an array of achievementId that isTask */ public listTaskIds() { - return this.getAchievements() + return this.getAllAchievement() .filter(achievement => achievement.isTask) .map(task => task.id); } @@ -166,7 +193,7 @@ class AchievementInferencer { * Returns a sorted array of achievementId that isTask */ public listTaskIdsbyPosition() { - return this.getAchievements() + return this.getAllAchievement() .filter(achievement => achievement.isTask) .sort((taskA, taskB) => taskA.position - taskB.position) .map(sortedTask => sortedTask.id); @@ -176,7 +203,7 @@ class AchievementInferencer { * Returns an array of achievementId that not isTask */ public listNonTaskIds() { - return this.getAchievements() + return this.getAllAchievement() .filter(achievement => !achievement.isTask) .map(nonTask => nonTask.id); } @@ -279,7 +306,7 @@ class AchievementInferencer { * Returns total EXP earned from all goals */ public getTotalExp() { - return [...this.goalList.values()].reduce((totalExp, goal) => totalExp + goal.exp, 0); + return this.getAllGoal().reduce((totalExp, goal) => totalExp + goal.exp, 0); } /** @@ -374,7 +401,7 @@ class AchievementInferencer { * @param newPosition the new position */ public changeAchievementPosition(achievement: AchievementItem, newPosition: number) { - const achievements = this.getAchievements() + const achievements = this.getAllAchievement() .filter(achievement => achievement.isTask) .sort((taskA, taskB) => taskA.position - taskB.position); @@ -540,7 +567,7 @@ class AchievementInferencer { */ private normalizePositions() { const posToId = new Map(); - this.getAchievements().forEach(achievement => + this.getAllAchievement().forEach(achievement => posToId.set(achievement.position, achievement.id) ); diff --git a/src/commons/achievement/utils/__tests__/Inferencer.test.ts b/src/commons/achievement/utils/__tests__/Inferencer.test.ts index d403c7d8be..dd2bf9f040 100644 --- a/src/commons/achievement/utils/__tests__/Inferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/Inferencer.test.ts @@ -45,13 +45,13 @@ describe('Achievements change when', () => { const inferencer = new AchievementInferencer(mockAchievements, mockGoals); inferencer.insertAchievement(sampleAchievement); - expect(inferencer.getAchievements().length).toEqual(13); + expect(inferencer.getAllAchievement().length).toEqual(13); expect(inferencer.doesAchievementExist(sampleAchievement.id)).toEqual(true); expect(inferencer.getAchievementItem(sampleAchievement.id)).toEqual(sampleAchievement); inferencer.removeAchievement(sampleAchievement.id); - expect(inferencer.getAchievements().length).toEqual(12); + expect(inferencer.getAllAchievement().length).toEqual(12); }); test('an achievement swaps position', () => { diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 10c28e0f68..faf9a8b9f7 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -20,10 +20,11 @@ export type StateProps = { function AchievementControl(props: DispatchProps & StateProps) { const { - inferencer, handleBulkUpdateAchievements, + handleBulkUpdateGoals, handleGetAchievements, - handleGetOwnGoals + handleGetOwnGoals, + inferencer } = props; /** @@ -35,8 +36,6 @@ function AchievementControl(props: DispatchProps & StateProps) { handleGetAchievements(); handleGetOwnGoals(); } - console.log('fetch achievements'); - console.log('fetch goals'); }, [handleGetAchievements, handleGetOwnGoals]); // TODO: @@ -46,6 +45,10 @@ function AchievementControl(props: DispatchProps & StateProps) { */ const publishState = useState(false); + /** + * forceRender allows child components to trigger a page render, + * so that the AchievementPreview displays the latest local changes + */ const [render, setRender] = useState(); const forceRender = () => setRender(!render); @@ -53,14 +56,15 @@ function AchievementControl(props: DispatchProps & StateProps) {
From b5b9d9e0707d12c8200643a7cfa5ae4d3921d1f5 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 9 Aug 2020 17:01:22 +0800 Subject: [PATCH 008/143] Use isNaN as focusId default state --- src/commons/achievement/AchievementView.tsx | 11 ++++++++++- .../achievement/control/AchievementEditor.tsx | 8 ++++---- .../achievement/control/AchievementPreview.tsx | 15 +++++---------- .../editorTools/EditableAchievementCard.tsx | 14 +++++--------- .../editableUtils/AchievementAdder.tsx | 4 +++- .../subcomponents/AchievementDashboard.tsx | 2 +- src/styles/_achievementdashboard.scss | 12 ++++++++++++ 7 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/commons/achievement/AchievementView.tsx b/src/commons/achievement/AchievementView.tsx index bf64890135..16a6b9fce8 100644 --- a/src/commons/achievement/AchievementView.tsx +++ b/src/commons/achievement/AchievementView.tsx @@ -1,3 +1,5 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import React from 'react'; import { @@ -18,7 +20,14 @@ type AchievementViewProps = { function AchievementView(props: AchievementViewProps) { const { inferencer, focusId } = props; - if (focusId < 0) return null; + if (isNaN(focusId)) { + return ( +
+ +

Select an achievement

+
+ ); + } const achievement = inferencer.getAchievementItem(focusId); const { ability, deadline, title, view } = achievement; diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index 0bc1ceb98a..77cf5a0b13 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -17,15 +17,15 @@ function AchievementEditor(props: AchievementEditorProps) { * NOTE: This helps us to ensure that only ONE achievement is added * every time. * - * Refering to AchievementAdder, if the adderId is -1, this + * Refering to AchievementAdder, if the controlId is NaN, this * means that currently no achievement is being added and the admin is able to * add a new achievement. * - * Alternatievly, if the adderId is not -1, this means that currently an achievement - * is being added to the systen and the admin is not allowed to add two achievements + * Alternatievly, if the controlId is not NaN, this means that currently an achievement + * is being added to the system and the admin is not allowed to add two achievements * at one go. */ - const controlState = useState(-1); + const controlState = useState(NaN); const generateEditableCards = (inferencer: AchievementInferencer) => { const achievementIds = inferencer.listIds().reverse(); diff --git a/src/commons/achievement/control/AchievementPreview.tsx b/src/commons/achievement/control/AchievementPreview.tsx index 73420be828..c0d00bb85e 100644 --- a/src/commons/achievement/control/AchievementPreview.tsx +++ b/src/commons/achievement/control/AchievementPreview.tsx @@ -1,4 +1,4 @@ -import { Button, Icon } from '@blueprintjs/core'; +import { Button } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; import { @@ -26,16 +26,18 @@ function AchievementPreview(props: AchievementPreviewProps) { const [canPublish, setCanPublish] = publishState; const handlePublish = () => { + // Update goals first because goals must exist before their ID can be specified in achievements publishGoals(goals); publishAchievements(achievements); setCanPublish(false); }; + // The Preview displays the AchievementView on View mode const [viewMode, setViewMode] = useState(false); const toggleMode = () => setViewMode(!viewMode); // If an achievement is focused, the cards glow - const focusState = useState(-1); + const focusState = useState(NaN); const [focusId] = focusState; return ( @@ -60,14 +62,7 @@ function AchievementPreview(props: AchievementPreviewProps) {
{viewMode ? (
- {focusId < 0 ? ( -
- -

Select an achievement

-
- ) : ( - - )} +
) : (
    diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx index 2a61a1faf1..bd1699a83c 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx @@ -52,15 +52,10 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { setIsDirty(false); setCanPublish(true); forceRender(); - /** - * It means that there is currently an item being added. - * Once this item is added, we can reset it to -1, which means - * that there is no achievement being added. - * indicating to the system that there is no achievement - * being added. - */ + + // Release the controlId if (id === controlId) { - setControlId(-1); + setControlId(NaN); } }; @@ -74,8 +69,9 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { setCanPublish(true); forceRender(); + // Release the controlId if (id === controlId) { - setControlId(-1); + setControlId(NaN); } }; diff --git a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx b/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx index aad15be1a4..efb8660fec 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx +++ b/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx @@ -16,10 +16,12 @@ function AchievementAdder(props: AchievementAdderProps) { const handleAddAchievement = () => { const createdId = inferencer.insertAchievement(achievementTemplate); + // Mark this new achievementId as controlId, it will only get released + // when this achievement is saved into the inferencer setControlId(createdId); }; - const disableAdder = controlId !== -1; + const disableAdder = !isNaN(controlId); return ( - {generateDeadlineString()} - - - - setOpen(!isOpen)} isOpen={isOpen} title={`Edit Achievement ${type}`}> + + - ); diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableTools.tsx b/src/commons/achievement/control/editorTools/editableUtils/EditableTools.tsx new file mode 100644 index 0000000000..4edadbec6f --- /dev/null +++ b/src/commons/achievement/control/editorTools/editableUtils/EditableTools.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +type EditableToolsProps = { + changeCardBackground: (cardBackground: string) => void; + changePosition: (position: number) => void; + goalIds: number[]; + position: number; + prerequisiteIds: number[]; +}; + +function EditableTools(props: EditableToolsProps) { + return <>; +} + +export default EditableTools; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementUploader.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementUploader.tsx deleted file mode 100644 index 0e3383f4b6..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementUploader.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import AchievementUploader from '../AchievementUploader'; - -const mockProps = { - hasChanges: false, - saveChanges: () => {}, - discardChanges: () => {}, - pendingUpload: false, - uploadChanges: () => {} -}; - -test('AchievementUploader component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap index 75462d501b..da8b963ee9 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap @@ -2,26 +2,28 @@ exports[`AchievementDeleter component renders correctly 1`] = ` " - - - - - - + + + + + + + " `; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementUploader.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementUploader.tsx.snap deleted file mode 100644 index 45109f2911..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementUploader.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AchievementUploader component renders correctly 1`] = `""`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap index 741ce17e0d..c09d2185b5 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap @@ -3,27 +3,25 @@ exports[`EditableAchievementAbility component renders correctly 1`] = ` "
    - - - + + + - +
    -
    - - - -
    + + +
    diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap index 85a346e1ef..2ef39ad3e5 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap @@ -2,20 +2,18 @@ exports[`EditableAchievementBackground component renders correctly 1`] = ` " -
    -
    - - - -
    - - +
    + + + + +
    " diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap index 3f0dc13bbe..ff281dd678 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap @@ -3,36 +3,17 @@ exports[`EditableAchievementDate component renders correctly 1`] = ` "
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    - - + + + + +
    " diff --git a/src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx b/src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx index 1f37eec9d1..66e0ca5bf5 100644 --- a/src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx +++ b/src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx @@ -1,76 +1,59 @@ -import { Button, Card, Dialog } from '@blueprintjs/core'; +import { Button, Dialog, EditableText } from '@blueprintjs/core'; import React, { useState } from 'react'; import { AchievementView } from '../../../../../features/achievement/AchievementTypes'; -import EditableViewDescription from './EditableViewDescription'; -import EditableViewImage from './EditableViewImage'; -import EditableViewText from './EditableViewText'; type EditableAchievementViewProps = { - title: string; view: AchievementView; changeView: any; }; function EditableAchievementView(props: EditableAchievementViewProps) { - const { title, view, changeView } = props; + const { view, changeView } = props; - const [isDialogOpen, setDialogOpen] = useState(false); + const [isOpen, setOpen] = useState(false); + const toggleOpen = () => setOpen(!isOpen); - const setDescription = (newDescription: string) => { - const newView = { - description: newDescription, - canvasUrl: view.canvasUrl, - completionText: view.completionText - }; - changeView(newView); + const { canvasUrl, description, completionText } = view; + + const changeCanvasUrl = (canvasUrl: string) => { + changeView({ ...view, canvasUrl: canvasUrl }); }; - const setCompletionText = (newCompletionText: string) => { - const newView = { - description: view.description, - canvasUrl: view.canvasUrl, - completionText: newCompletionText - }; - changeView(newView); + const changeDescription = (description: string) => { + changeView({ ...view, description: description }); }; - const setCanvasUrl = (newCanvasUrl: string) => { - const newView = { - description: view.description, - canvasUrl: newCanvasUrl, - completionText: view.completionText - }; - changeView(newView); + const changeCompletionText = (completionText: string) => { + changeView({ ...view, completionText: completionText }); }; return ( -
    -
    -
    - setDialogOpen(!isDialogOpen)} - isOpen={isDialogOpen} - title={'Edit View'} - usePortal={false} - > -
    - -

    {title}

    - - - - -
    -
    +
    +
    ); diff --git a/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx b/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx index d1b0d1c398..2e12aabcc3 100644 --- a/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx +++ b/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx @@ -3,12 +3,11 @@ import React, { useState } from 'react'; type EditableViewImageProps = { canvasUrl: string; - title: string; setCanvasUrl: any; }; function EditableViewImage(props: EditableViewImageProps) { - const { canvasUrl, title, setCanvasUrl } = props; + const { canvasUrl, setCanvasUrl } = props; const [isEditing, setIsEditing] = useState(false); @@ -21,12 +20,12 @@ function EditableViewImage(props: EditableViewImageProps) { {isEditing ? ( ) : ( - {title} + {'view )}
    ); diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap index d541bc9d29..96f148c0dc 100644 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap @@ -2,20 +2,18 @@ exports[`EditableAchievementView component renders correctly 1`] = ` " -
    -
    - - - -
    - - +
    + + + + +
    " diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap index a3e6273cca..7c0e129e81 100644 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap +++ b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap @@ -12,7 +12,7 @@ exports[`EditableViewImage component renders correctly 1`] = ` - \\"Sample + \\"view
    " `; diff --git a/src/commons/achievement/utils/AchievementCard.tsx b/src/commons/achievement/utils/AchievementCard.tsx index ce2d3fff2b..3f87e98f65 100644 --- a/src/commons/achievement/utils/AchievementCard.tsx +++ b/src/commons/achievement/utils/AchievementCard.tsx @@ -23,7 +23,6 @@ function AchievementCard(props: AchievementCardProps) { const [focusId, setFocusId] = focusState; const { ability, cardTileUrl, title } = inferencer.getAchievementItem(id); - const displayDeadline = inferencer.getDisplayDeadline(id); const displayExp = inferencer.getMaxExp(id); const progressFrac = inferencer.getProgressFrac(id); diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index bbbad5651d..d5189a8e6d 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -200,31 +200,6 @@ class AchievementInferencer { .map(nonTask => nonTask.id); } - /** - * Set the achievement item as isTask - * - * @param achievement the AchievementItem - */ - public setTask(achievement: AchievementItem) { - achievement.isTask = true; - achievement.position = this.listTaskIds().length + 1; - - this.modifyAchievement(achievement); - } - - /** - * Set the achievement item as not isTask - * - * @param achievement the AchievementItem - */ - public setNonTask(achievement: AchievementItem) { - achievement.prerequisiteIds = []; - achievement.isTask = false; - achievement.position = 0; // position 0 is reserved for non-task achievements - - this.modifyAchievement(achievement); - } - /** * Returns an array of AchievementGoal which belongs to the achievement * @@ -392,7 +367,9 @@ class AchievementInferencer { * @param achievement the AchievementItem * @param newPosition the new position */ - public changeAchievementPosition(achievement: AchievementItem, newPosition: number) { + public changePosition(achievement: AchievementItem, newPosition: number) { + achievement.isTask = newPosition !== 0; + const achievements = this.getAllAchievement() .filter(achievement => achievement.isTask) .sort((taskA, taskB) => taskA.position - taskB.position); @@ -404,6 +381,8 @@ class AchievementInferencer { const editedAchievement = achievements[i]; editedAchievement.position = i + 1; } + + this.normalizePositions(); } /** diff --git a/src/commons/achievement/utils/__tests__/Inferencer.test.ts b/src/commons/achievement/utils/__tests__/Inferencer.test.ts index dd2bf9f040..399dcea62c 100644 --- a/src/commons/achievement/utils/__tests__/Inferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/Inferencer.test.ts @@ -24,23 +24,6 @@ const sampleAchievement: AchievementItem = { }; describe('Achievements change when', () => { - test('an achievement is unset to be a task', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - inferencer.setNonTask(inferencer.getAchievementItem(0)); - - expect(inferencer.getAchievementItem(0).isTask).toEqual(false); - expect(inferencer.getAchievementItem(0).position).toEqual(0); - expect(inferencer.getAchievementItem(0).prerequisiteIds).toEqual([]); - }); - - test('an achievement is set to be a task', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - inferencer.setTask(inferencer.getAchievementItem(0)); - - expect(inferencer.getAchievementItem(0).isTask).toEqual(true); - expect(inferencer.getAchievementItem(0).position).toEqual(inferencer.listTaskIds().length); - }); - test('an achievement is inserted and deleted', () => { const inferencer = new AchievementInferencer(mockAchievements, mockGoals); @@ -58,25 +41,8 @@ describe('Achievements change when', () => { const inferencer = new AchievementInferencer(mockAchievements, mockGoals); const firstTask = inferencer.getAchievementItem(4); - inferencer.changeAchievementPosition(firstTask, 2); + inferencer.changePosition(firstTask, 2); expect(inferencer.getAchievementItem(firstTask.id).position).toEqual(2); }); }); - -describe('Children are listed', () => { - test('to test if they are immediate', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - const firstAchievementId = inferencer.getAchievementItem(0).id; - const secondAchievementId = inferencer.getAchievementItem(1).id; - - expect(inferencer.isImmediateChild(firstAchievementId, secondAchievementId)).toEqual(false); - }); - - test('if listImmediateChildren is called', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - const firstAchievementId = inferencer.getAchievementItem(0).id; - - expect([...inferencer.getImmediateChildren(firstAchievementId)]).toEqual([]); - }); -}); From 13180f62d4a43358382f49b61e21f9695b9dbe9f Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 12 Aug 2020 05:04:03 +0800 Subject: [PATCH 017/143] Fix test --- .../EditableAchievementBackground.tsx | 15 -------------- .../__tests__/EditableAchievementDate.tsx | 16 --------------- .../EditableAchievementBackground.tsx.snap | 20 ------------------- .../EditableAchievementDate.tsx.snap | 20 ------------------- 4 files changed, 71 deletions(-) delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementBackground.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementDate.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementBackground.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementBackground.tsx deleted file mode 100644 index 507ea0570c..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementBackground.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableAchievementBackground from '../EditableAchievementBackground'; - -const mockProps = { - cardTileUrl: '', - setcardTileUrl: () => {} -}; - -test('EditableAchievementBackground component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementDate.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementDate.tsx deleted file mode 100644 index b3e5959fd1..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementDate.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableAchievementDate from '../EditableAchievementDate'; - -const mockProps = { - type: 'Deadline', - deadline: new Date(), - changeDeadline: () => {} -}; - -test('EditableAchievementDate component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap deleted file mode 100644 index 2ef39ad3e5..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementBackground.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementBackground component renders correctly 1`] = ` -" -
    - - - - - - -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap deleted file mode 100644 index ff281dd678..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementDate.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementDate component renders correctly 1`] = ` -" -
    - - - - - - -
    -
    " -`; From 30a96215e58a32f6c0821f5c9e5319134f19f8bf Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 12 Aug 2020 05:16:43 +0800 Subject: [PATCH 018/143] Remove unused editable components --- .../editorTools/EditableAchievementCard.tsx | 2 +- .../EditableAchievementView.tsx | 0 .../editableView/EditableViewDescription.tsx | 26 -------------- .../editableView/EditableViewImage.tsx | 34 ------------------- .../editableView/EditableViewText.tsx | 24 ------------- .../__tests__/EditableAchievementView.tsx | 17 ---------- .../__tests__/EditableViewDescription.tsx | 15 -------- .../__tests__/EditableViewImage.tsx | 17 ---------- .../__tests__/EditableViewText.tsx | 15 -------- .../EditableAchievementView.tsx.snap | 20 ----------- .../EditableViewDescription.tsx.snap | 15 -------- .../__snapshots__/EditableViewImage.tsx.snap | 18 ---------- .../__snapshots__/EditableViewText.tsx.snap | 13 ------- 13 files changed, 1 insertion(+), 215 deletions(-) rename src/commons/achievement/control/editorTools/{editableView => editableUtils}/EditableAchievementView.tsx (100%) delete mode 100644 src/commons/achievement/control/editorTools/editableView/EditableViewDescription.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/EditableViewText.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/EditableAchievementView.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewDescription.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewImage.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewText.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewDescription.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewText.tsx.snap diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx index 560dfe4d31..b58c7b8a88 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx @@ -12,8 +12,8 @@ import AchievementSaver from './editableUtils/AchievementSaver'; import EditableAchievementAbility from './editableUtils/EditableAchievementAbility'; import EditableAchievementDate from './editableUtils/EditableAchievementDate'; import EditableAchievementTitle from './editableUtils/EditableAchievementTitle'; +import EditableAchievementView from './editableUtils/EditableAchievementView'; import EditableTools from './editableUtils/EditableTools'; -import EditableAchievementView from './editableView/EditableAchievementView'; type EditableAchievementCardProps = { id: number; diff --git a/src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx b/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/editableView/EditableAchievementView.tsx rename to src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx diff --git a/src/commons/achievement/control/editorTools/editableView/EditableViewDescription.tsx b/src/commons/achievement/control/editorTools/editableView/EditableViewDescription.tsx deleted file mode 100644 index 495ab454d1..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/EditableViewDescription.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { EditableText } from '@blueprintjs/core'; -import React from 'react'; - -type EditableViewDescriptionProps = { - description: string; - setDescription: any; -}; - -function EditableViewDescription(props: EditableViewDescriptionProps) { - const { description, setDescription } = props; - - return ( - <> -

    - -

    - - ); -} - -export default EditableViewDescription; diff --git a/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx b/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx deleted file mode 100644 index 2e12aabcc3..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/EditableViewImage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Button, EditableText } from '@blueprintjs/core'; -import React, { useState } from 'react'; - -type EditableViewImageProps = { - canvasUrl: string; - setCanvasUrl: any; -}; - -function EditableViewImage(props: EditableViewImageProps) { - const { canvasUrl, setCanvasUrl } = props; - - const [isEditing, setIsEditing] = useState(false); - - return ( -
    -
    - ); -} - -export default EditableViewImage; diff --git a/src/commons/achievement/control/editorTools/editableView/EditableViewText.tsx b/src/commons/achievement/control/editorTools/editableView/EditableViewText.tsx deleted file mode 100644 index c371006d77..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/EditableViewText.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { EditableText } from '@blueprintjs/core'; -import React from 'react'; - -type EditableViewTextProps = { - goalText: string; - setGoalText: any; -}; - -function EditableViewText(props: EditableViewTextProps) { - const { goalText, setGoalText } = props; - - return ( - <> - - - ); -} - -export default EditableViewText; diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableAchievementView.tsx b/src/commons/achievement/control/editorTools/editableView/__tests__/EditableAchievementView.tsx deleted file mode 100644 index 6d2ef3f9fb..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableAchievementView.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockAchievements } from '../../../../../mocks/AchievementMocks'; -import EditableAchievementView from '../EditableAchievementView'; - -const mockProps = { - title: 'Sample Title', - view: mockAchievements[0].view, - changeView: () => {} -}; - -test('EditableAchievementView component renders correctly', () => { - const view = ; - const tree = mount(view); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewDescription.tsx b/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewDescription.tsx deleted file mode 100644 index bfbb56735f..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewDescription.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableViewDescription from '../EditableViewDescription'; - -const mockProps = { - description: 'Sample Description', - setDescription: () => {} -}; - -test('EditableViewDescription component renders correctly', () => { - const description = ; - const tree = mount(description); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewImage.tsx b/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewImage.tsx deleted file mode 100644 index ca36d6ba53..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewImage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableViewImage from '../EditableViewImage'; - -const mockProps = { - canvasUrl: - 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/locations/planet-y-orbit/crashing.png', - title: 'Sample Title', - setCanvasUrl: () => {} -}; - -test('EditableViewImage component renders correctly', () => { - const image = ; - const tree = mount(image); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewText.tsx b/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewText.tsx deleted file mode 100644 index 5f82e33d37..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/EditableViewText.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableViewText from '../EditableViewText'; - -const mockProps = { - goalText: 'Goal Text', - setGoalText: () => {} -}; - -test('EditableViewText component renders correctly', () => { - const text = ; - const tree = mount(text); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap deleted file mode 100644 index 96f148c0dc..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableAchievementView.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementView component renders correctly 1`] = ` -" -
    - - - - - - -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewDescription.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewDescription.tsx.snap deleted file mode 100644 index a6d237772d..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewDescription.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableViewDescription component renders correctly 1`] = ` -" -

    - -
    - - Sample Description - -
    -
    -

    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap deleted file mode 100644 index 7c0e129e81..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewImage.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableViewImage component renders correctly 1`] = ` -" -
    - - - - \\"view -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewText.tsx.snap b/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewText.tsx.snap deleted file mode 100644 index 44cf629f0a..0000000000 --- a/src/commons/achievement/control/editorTools/editableView/__tests__/__snapshots__/EditableViewText.tsx.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableViewText component renders correctly 1`] = ` -" - -
    - - Goal Text - -
    -
    -
    " -`; From 42bede2ba0f748fd924358fe129565df06c7f086 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 12 Aug 2020 17:25:21 +0800 Subject: [PATCH 019/143] Implement change position, remove redundant items --- .../achievement/control/ControlPanel.tsx | 50 -------- .../AchievementControlPanelTools.tsx | 59 --------- .../EditableAchievementTask.tsx | 30 ----- .../AchievementControlPanelTools.tsx | 19 --- .../AchievementControlPanelTools.tsx.snap | 110 ---------------- .../AchievementSelection.tsx | 107 ---------------- .../controlPanelUtils/PrerequisiteAdder.tsx | 68 ---------- .../controlPanelUtils/PrerequisiteDeleter.tsx | 61 --------- .../PrerequisitePositionEditor.tsx | 69 ---------- .../controlPanelUtils/PrerequisiteSwapper.tsx | 118 ------------------ .../controlPanelUtils/TaskAdder.tsx | 55 -------- .../controlPanelUtils/TaskDeleter.tsx | 38 ------ .../TaskPositionInserter.tsx | 63 ---------- .../controlPanelUtils/TaskUploader.tsx | 27 ---- .../__tests__/PrerequisiteAdder.tsx | 19 --- .../__tests__/PrerequisiteDeleter.tsx | 19 --- .../__tests__/PrerequisitePositionEditor.tsx | 18 --- .../__tests__/PrerequisiteSwapper.tsx | 26 ---- .../controlPanelUtils/__tests__/TaskAdder.tsx | 17 --- .../__tests__/TaskDeleter.tsx | 18 --- .../__tests__/TaskPositionInserter.tsx | 19 --- .../__tests__/TaskUploader.tsx | 15 --- .../__snapshots__/PrerequisiteAdder.tsx.snap | 20 --- .../PrerequisiteDeleter.tsx.snap | 20 --- .../PrerequisitePositionEditor.tsx.snap | 20 --- .../PrerequisiteSwapper.tsx.snap | 9 -- .../__snapshots__/TaskAdder.tsx.snap | 20 --- .../__snapshots__/TaskDeleter.tsx.snap | 15 --- .../TaskPositionInserter.tsx.snap | 47 ------- .../__snapshots__/TaskUploader.tsx.snap | 3 - .../editorTools/EditableAchievementCard.tsx | 8 +- .../EditableAchievementBackground.tsx | 32 ----- .../editableUtils/EditableAchievementView.tsx | 12 +- .../editableUtils/EditableTools.tsx | 58 ++++++++- .../__tests__/AchievementDeleter.tsx | 14 --- .../__tests__/EditableAchievementAbility.tsx | 16 --- .../__tests__/EditableAchievementExp.tsx | 15 --- .../__tests__/EditableAchievementTitle.tsx | 15 --- .../__snapshots__/AchievementDeleter.tsx.snap | 29 ----- .../EditableAchievementAbility.tsx.snap | 38 ------ .../EditableAchievementExp.tsx.snap | 28 ----- .../EditableAchievementTitle.tsx.snap | 17 --- .../utils/AchievementInferencer.ts | 53 +++----- .../utils/__tests__/Inferencer.test.ts | 9 -- 44 files changed, 83 insertions(+), 1440 deletions(-) delete mode 100644 src/commons/achievement/control/ControlPanel.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/AchievementControlPanelTools.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/EditableAchievementTask.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/__tests__/AchievementControlPanelTools.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/__tests__/__snapshots__/AchievementControlPanelTools.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/AchievementSelection.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteAdder.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteDeleter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisitePositionEditor.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteSwapper.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskAdder.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskDeleter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskPositionInserter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskUploader.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/PrerequisiteAdder.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/PrerequisiteDeleter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/PrerequisitePositionEditor.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/PrerequisiteSwapper.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/TaskAdder.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/TaskDeleter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/TaskPositionInserter.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/TaskUploader.tsx delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteAdder.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteDeleter.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisitePositionEditor.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteSwapper.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskAdder.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskDeleter.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskPositionInserter.tsx.snap delete mode 100644 src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskUploader.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/EditableAchievementBackground.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementDeleter.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementAbility.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementExp.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementTitle.tsx delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementExp.tsx.snap delete mode 100644 src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementTitle.tsx.snap diff --git a/src/commons/achievement/control/ControlPanel.tsx b/src/commons/achievement/control/ControlPanel.tsx deleted file mode 100644 index 866f73f2cf..0000000000 --- a/src/commons/achievement/control/ControlPanel.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; - -import AchievementInferencer from '../utils/AchievementInferencer'; -import TaskAdder from './controlPanelTools/controlPanelUtils/TaskAdder'; -import TaskUploader from './controlPanelTools/controlPanelUtils/TaskUploader'; - -type ControlPanelProps = { - inferencer: AchievementInferencer; - updateAchievements: any; - forceRender: any; - isDisabled: boolean; - - pendingUpload: any; - setPendingUpload: any; - saveAchievementsToFrontEnd: any; -}; - -function ControlPanel(props: ControlPanelProps) { - const { - inferencer, - updateAchievements, - forceRender, - pendingUpload, - setPendingUpload, - saveAchievementsToFrontEnd - } = props; - - const handleSaveChanges = () => { - setPendingUpload(true); - saveAchievementsToFrontEnd(inferencer.getAllAchievement()); - forceRender(); - }; - - const handleUploadChanges = () => { - updateAchievements(); - setPendingUpload(false); - forceRender(); - }; - - return ( -
    -
    - - -
    -
    - ); -} - -export default ControlPanel; diff --git a/src/commons/achievement/control/controlPanelTools/AchievementControlPanelTools.tsx b/src/commons/achievement/control/controlPanelTools/AchievementControlPanelTools.tsx deleted file mode 100644 index e8823e4ee5..0000000000 --- a/src/commons/achievement/control/controlPanelTools/AchievementControlPanelTools.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { AchievementItem } from '../../../../features/achievement/AchievementTypes'; -import AchievementInferencer from '../../utils/AchievementInferencer'; -import PrerequisiteAdder from './controlPanelUtils/PrerequisiteAdder'; -import PrerequisiteDeleter from './controlPanelUtils/PrerequisiteDeleter'; -import PrerequisitePositionEditor from './controlPanelUtils/PrerequisitePositionEditor'; -import TaskDeleter from './controlPanelUtils/TaskDeleter'; -import TaskPositionInserter from './controlPanelUtils/TaskPositionInserter'; - -type AchievementControlPanelToolsProps = { - editableAchievement: AchievementItem; - setEditableAchievement: any; - inferencer: AchievementInferencer; - saveChanges: any; -}; - -function AchievementControlPanelTools(props: AchievementControlPanelToolsProps) { - const { editableAchievement, setEditableAchievement, inferencer, saveChanges } = props; - - return ( -
    - - - - - - - - - -
    - ); -} - -export default AchievementControlPanelTools; diff --git a/src/commons/achievement/control/controlPanelTools/EditableAchievementTask.tsx b/src/commons/achievement/control/controlPanelTools/EditableAchievementTask.tsx deleted file mode 100644 index f942aeda01..0000000000 --- a/src/commons/achievement/control/controlPanelTools/EditableAchievementTask.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useState } from 'react'; - -import { AchievementItem } from '../../../../features/achievement/AchievementTypes'; -import AchievementInferencer from '../../utils/AchievementInferencer'; -import AchievementControlPanelTools from './AchievementControlPanelTools'; - -type EditableAchievementTaskProps = { - achievement: AchievementItem; - inferencer: AchievementInferencer; - saveChanges: any; -}; - -function EditableAchievementTask(props: EditableAchievementTaskProps) { - const { inferencer, achievement, saveChanges } = props; - - const [editableAchievement, setEditableAchievement] = useState(achievement); - - return ( -
    - -
    - ); -} - -export default EditableAchievementTask; diff --git a/src/commons/achievement/control/controlPanelTools/__tests__/AchievementControlPanelTools.tsx b/src/commons/achievement/control/controlPanelTools/__tests__/AchievementControlPanelTools.tsx deleted file mode 100644 index dc16bb64b2..0000000000 --- a/src/commons/achievement/control/controlPanelTools/__tests__/AchievementControlPanelTools.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockAchievements, mockGoals } from '../../../../mocks/AchievementMocks'; -import AchievementInferencer from '../../../utils/AchievementInferencer'; -import AchievementControlPanelTools from '../AchievementControlPanelTools'; - -const mockProps = { - editableAchievement: mockAchievements[0], - setEditableAchievement: () => {}, - inferencer: new AchievementInferencer(mockAchievements, mockGoals), - saveChanges: () => {} -}; - -test('AchievementControlPanelTools component renders correctly', () => { - const component = ; - const tree = mount(component); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/controlPanelTools/__tests__/__snapshots__/AchievementControlPanelTools.tsx.snap b/src/commons/achievement/control/controlPanelTools/__tests__/__snapshots__/AchievementControlPanelTools.tsx.snap deleted file mode 100644 index e67ffb50c3..0000000000 --- a/src/commons/achievement/control/controlPanelTools/__tests__/__snapshots__/AchievementControlPanelTools.tsx.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AchievementControlPanelTools component renders correctly 1`] = ` -" -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - -
    - - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    -
    - - - - - - - - - - -
    -
    " -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/AchievementSelection.tsx b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/AchievementSelection.tsx deleted file mode 100644 index faecb06486..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/AchievementSelection.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button, Classes, Dialog, MenuItem } from '@blueprintjs/core'; -import { ItemRenderer, Select } from '@blueprintjs/select'; -import React from 'react'; - -import AchievementInferencer from '../../../utils/AchievementInferencer'; - -type AchievementSelectionProps = { - inferencer: AchievementInferencer; - dialogHeader: string; - selections: number[]; - selectedId: number; - setSelectedId: any; - buttonText: string; - emptySelectionsMessage: string; - action: any; - isDialogOpen: boolean; - toggleDialogOpen: any; -}; - -function AchievementSelection(props: AchievementSelectionProps) { - const { - inferencer, - dialogHeader, - selections, - selectedId, - setSelectedId, - buttonText, - emptySelectionsMessage, - action, - isDialogOpen, - toggleDialogOpen - } = props; - - let shouldDisabledAction = false; - - const getAchievementTitle = (id: number) => { - if (!inferencer.doesAchievementExist(id)) { - shouldDisabledAction = true; - return 'Please Select an Item'; - } - return inferencer.getAchievementItem(id).title; - }; - - const changeSelectedId = (selectedId: number) => { - setSelectedId(selectedId); - }; - - const selectionsRenderer: ItemRenderer = (id, { handleClick }) => { - return ( - - ); - }; - - const SelectionsComponent = Select.ofType(); - - const selectButton = (currentPrerequisiteID: number) => { - return ( -
    -
    - ); - }; - - return ( - <> - -
    - {selections.length === 0 ? ( -
    -

    {emptySelectionsMessage}

    -
    - ) : ( - <> -
    -
    - - {selectButton(selectedId)} - -
    -
    -
    -
    - - )} -
    -
    - - ); -} - -export default AchievementSelection; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteAdder.tsx b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteAdder.tsx deleted file mode 100644 index ba48cd4c3f..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/PrerequisiteAdder.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Button } from '@blueprintjs/core'; -import React, { useState } from 'react'; - -import { AchievementItem } from '../../../../../features/achievement/AchievementTypes'; -import AchievementInferencer from '../../../utils/AchievementInferencer'; -import AchievementSelection from './AchievementSelection'; - -type PrerequisiteAdderProps = { - editableAchievement: AchievementItem; - setEditableAchievement: any; - inferencer: AchievementInferencer; - saveChanges: any; -}; - -function PrerequisiteAdder(props: PrerequisiteAdderProps) { - const { editableAchievement, setEditableAchievement, inferencer, saveChanges } = props; - - const [isDialogOpen, setDialogOpen] = useState(false); - const toggleDialogOpen = () => setDialogOpen(!isDialogOpen); - - const nonPrerequisites = inferencer.listAvailablePrerequisites(editableAchievement.id); - - const addPrerequisite = (prerequisiteID: number) => { - const newAchievement = editableAchievement; - newAchievement.prerequisiteIds.push(prerequisiteID); - return newAchievement; - }; - - /** - * If there are no more prerequisites to add to this achievement, - * it will be set to a default value of 0. - * - * Else, it will always take the first available prerequisite id. - */ - const [addedPrerequisiteID, setAddedPrerequisiteID] = useState( - nonPrerequisites.length === -1 ? 0 : nonPrerequisites[0] - ); - - // This actions adds a new prerequisite to the achievement. - const addPrerequisiteAction = () => { - setEditableAchievement(addPrerequisite(addedPrerequisiteID)); - inferencer.modifyAchievement(editableAchievement); - - saveChanges(); - - toggleDialogOpen(); - }; - - return ( - <> -
    - ); - }; - - return ( - <> - -
    - {prerequisiteIdDs.length === 0 ? ( -
    -

    Add a prerequisite to continue

    -
    - ) : ( - <> -
    -
    - - {taskSelector(firstID)} - -
    - -
    - - {taskSelector(secondID)} - -
    - -
    -
    -
    - - )} -
    -
    - - ); -} - -export default PrerequisiteSwapper; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskAdder.tsx b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskAdder.tsx deleted file mode 100644 index 87e068a155..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskAdder.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Button } from '@blueprintjs/core'; -import React, { useState } from 'react'; - -import AchievementInferencer from '../../../utils/AchievementInferencer'; -import AchievementSelection from './AchievementSelection'; - -export type TaskAdderProps = { - inferencer: AchievementInferencer; - saveChanges: any; -}; - -function TaskAdder(props: TaskAdderProps) { - const { inferencer, saveChanges } = props; - - const [isDialogOpen, setDialogOpen] = useState(false); - const toggleDialogOpen = () => setDialogOpen(!isDialogOpen); - - const nonTaskIDs = inferencer.listNonTaskIds(); - - const [addedTaskID, setAddedTaskID] = useState( - nonTaskIDs.length === 0 ? 0 : nonTaskIDs[0] - ); - - const addNewTask = () => { - const achievement = inferencer.getAchievementItem(addedTaskID); - inferencer.changePosition(achievement, 0); - saveChanges(); - }; - - const addAction = () => { - addNewTask(); - - toggleDialogOpen(); - }; - - return ( - <> -
    - ); -} - -export default TaskPositionInserter; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskUploader.tsx b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskUploader.tsx deleted file mode 100644 index 5de22460c2..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/TaskUploader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Button } from '@blueprintjs/core'; -import React from 'react'; - -type TaskUploaderProps = { - pendingUpload: boolean; - uploadChanges: any; -}; - -function TaskUploader(props: TaskUploaderProps) { - const { pendingUpload, uploadChanges } = props; - - return ( - <> - {pendingUpload && ( - - - - - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteDeleter.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteDeleter.tsx.snap deleted file mode 100644 index 01f94c2372..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteDeleter.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrerequisiteDeleter component renders correctly 1`] = ` -" - - - - - - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisitePositionEditor.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisitePositionEditor.tsx.snap deleted file mode 100644 index e4049242b3..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisitePositionEditor.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrerequisitePositionEditor component renders correctly 1`] = ` -" - - - - - - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteSwapper.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteSwapper.tsx.snap deleted file mode 100644 index ed04b2ed7c..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/PrerequisiteSwapper.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PrerequisiteSwapper component renders correctly 1`] = ` -" - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskAdder.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskAdder.tsx.snap deleted file mode 100644 index f8fad4150c..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskAdder.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TaskAdder component renders correctly 1`] = ` -" - - - - - - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskDeleter.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskDeleter.tsx.snap deleted file mode 100644 index 0319dc2636..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskDeleter.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TaskDeleter component renders correctly 1`] = ` -" - - - -" -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskPositionInserter.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskPositionInserter.tsx.snap deleted file mode 100644 index 9e08fcb7da..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskPositionInserter.tsx.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TaskPositionInserter component renders correctly 1`] = ` -" -
    - - - - - - - - - -
    - - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    -
    " -`; diff --git a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskUploader.tsx.snap b/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskUploader.tsx.snap deleted file mode 100644 index b0c77fe09e..0000000000 --- a/src/commons/achievement/control/controlPanelTools/controlPanelUtils/__tests__/__snapshots__/TaskUploader.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TaskUploader component renders correctly 1`] = `""`; diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx index b58c7b8a88..bb686f0e5d 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx @@ -111,7 +111,12 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { }; const handleChangePosition = (position: number) => { - inferencer.changePosition(editableAchievement, position); + const isTask = position !== 0; + setEditableAchievement({ + ...editableAchievement, + isTask: isTask, + position: position + }); setIsDirty(true); }; @@ -133,6 +138,7 @@ function EditableAchievementCard(props: EditableAchievementCardProps) {
    (false); - const toggleDialog = () => setDialogOpen(!isDialogOpen); - - return ( -
    -
    - ); -} - -export default EditableAchievementBackground; diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx b/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx index 66e0ca5bf5..3485e0ce41 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx +++ b/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx @@ -29,27 +29,27 @@ function EditableAchievementView(props: EditableAchievementViewProps) { }; return ( -
    -
    + + ); } export default EditableTools; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementDeleter.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementDeleter.tsx deleted file mode 100644 index 0daa36e1df..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/AchievementDeleter.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import AchievementDeleter from '../AchievementDeleter'; - -const mockProps = { - deleteAchievement: () => {} -}; - -test('AchievementDeleter component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementAbility.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementAbility.tsx deleted file mode 100644 index fce144ba92..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementAbility.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import { AchievementAbility } from '../../../../../../features/achievement/AchievementTypes'; -import EditableAchievementAbility from '../EditableAchievementAbility'; - -const mockProps = { - ability: AchievementAbility.COMMUNITY, - changeAbility: () => {} -}; - -test('EditableAchievementAbility component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementExp.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementExp.tsx deleted file mode 100644 index 64912ec2f6..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementExp.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableAchievementExp from '../EditableAchievementExp'; - -const mockProps = { - exp: 0, - changeExp: () => {} -}; - -test('EditableAchievementExp component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementTitle.tsx b/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementTitle.tsx deleted file mode 100644 index 4aa06d83e9..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/EditableAchievementTitle.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { mount } from 'enzyme'; -import React from 'react'; - -import EditableAchievementTitle from '../EditableAchievementTitle'; - -const mockProps = { - title: '', - changeTitle: () => {} -}; - -test('EditableAchievementTitle component renders correctly', () => { - const goal = ; - const tree = mount(goal); - expect(tree.debug()).toMatchSnapshot(); -}); diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap deleted file mode 100644 index da8b963ee9..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/AchievementDeleter.tsx.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AchievementDeleter component renders correctly 1`] = ` -" -
    - - - - - - -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap deleted file mode 100644 index c09d2185b5..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementAbility.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementAbility component renders correctly 1`] = ` -" -
    - - - - - - - - - -
    - - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementExp.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementExp.tsx.snap deleted file mode 100644 index e2761b83a8..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementExp.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementExp component renders correctly 1`] = ` -" -
    - -
    - - - - - bank-account - - - - - - - -

    - XP -

    -
    -
    -
    -
    -
    " -`; diff --git a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementTitle.tsx.snap b/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementTitle.tsx.snap deleted file mode 100644 index 406fcd24ac..0000000000 --- a/src/commons/achievement/control/editorTools/editableUtils/__tests__/__snapshots__/EditableAchievementTitle.tsx.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditableAchievementTitle component renders correctly 1`] = ` -" -
    -

    - -
    - - Enter your title here - -
    -
    -

    -
    -
    " -`; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index d5189a8e6d..4101a6d62c 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -120,6 +120,7 @@ class AchievementInferencer { // finally, process the nodeList this.processNodes(); + this.normalizePositions(achievement.id); return newId; } @@ -135,6 +136,7 @@ class AchievementInferencer { // then, process the nodeList this.processNodes(); + this.normalizePositions(achievement.id); } /** @@ -163,6 +165,7 @@ class AchievementInferencer { // finally, process the nodeList this.processNodes(); + this.normalizePositions(); } /** @@ -359,32 +362,6 @@ class AchievementInferencer { ); } - /** - * Changes the position of the achievement - * - * Note: positions of achievements are 1-indexed - * - * @param achievement the AchievementItem - * @param newPosition the new position - */ - public changePosition(achievement: AchievementItem, newPosition: number) { - achievement.isTask = newPosition !== 0; - - const achievements = this.getAllAchievement() - .filter(achievement => achievement.isTask) - .sort((taskA, taskB) => taskA.position - taskB.position); - - const targetAchievement = achievements.splice(achievement.position - 1, 1)[0]; - achievements.splice(newPosition - 1, 0, targetAchievement); - - for (let i = Math.min(newPosition - 1, achievement.position); i < achievements.length; i++) { - const editedAchievement = achievements[i]; - editedAchievement.position = i + 1; - } - - this.normalizePositions(); - } - /** * Recalculates the InferencerNode data of each achievements, O(N) operation */ @@ -396,7 +373,6 @@ class AchievementInferencer { this.generateProgressFrac(node); this.generateStatus(node); }); - this.normalizePositions(); } /** @@ -536,20 +512,19 @@ class AchievementInferencer { /** * Reassign the achievement position number without changing their orders */ - private normalizePositions() { - const posToId = new Map(); - this.getAllAchievement().forEach(achievement => - posToId.set(achievement.position, achievement.id) - ); - - const sortedPosToId = [...posToId.entries()].sort(); + private normalizePositions(priorityId?: number) { + const sortedTasks = this.getAllAchievement() + .filter(achievement => achievement.isTask) + .sort((taskA, taskB) => + taskA.position === taskB.position + ? taskA.id === priorityId + ? -1 + : 1 + : taskA.position - taskB.position + ); let newPosition = 1; - for (const [pos, id] of sortedPosToId) { - if (pos !== 0) { - this.nodeList.get(id)!.achievement.position = newPosition++; - } - } + sortedTasks.forEach(task => (task.position = newPosition++)); } } diff --git a/src/commons/achievement/utils/__tests__/Inferencer.test.ts b/src/commons/achievement/utils/__tests__/Inferencer.test.ts index 399dcea62c..df31fa2d58 100644 --- a/src/commons/achievement/utils/__tests__/Inferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/Inferencer.test.ts @@ -36,13 +36,4 @@ describe('Achievements change when', () => { inferencer.removeAchievement(sampleAchievement.id); expect(inferencer.getAllAchievement().length).toEqual(12); }); - - test('an achievement swaps position', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - const firstTask = inferencer.getAchievementItem(4); - - inferencer.changePosition(firstTask, 2); - - expect(inferencer.getAchievementItem(firstTask.id).position).toEqual(2); - }); }); From 3b5eef3a957c703fa767a09f5bb675418d67007a Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 12 Aug 2020 17:40:23 +0800 Subject: [PATCH 020/143] Move publish handlers to achievement control --- .../control/AchievementPreview.tsx | 23 ++++--------------- .../control/AchievementControl.tsx | 17 +++++++++----- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/commons/achievement/control/AchievementPreview.tsx b/src/commons/achievement/control/AchievementPreview.tsx index 37b05daeef..1c91e33729 100644 --- a/src/commons/achievement/control/AchievementPreview.tsx +++ b/src/commons/achievement/control/AchievementPreview.tsx @@ -2,35 +2,20 @@ import { Button } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useContext, useState } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; -import { - AchievementItem, - FilterStatus, - GoalDefinition -} from 'src/features/achievement/AchievementTypes'; +import { FilterStatus } from 'src/features/achievement/AchievementTypes'; import { generateAchievementTasks } from 'src/pages/achievement/subcomponents/AchievementDashboard'; import AchievementView from '../AchievementView'; type AchievementPreviewProps = { - publishAchievements: (achievements: AchievementItem[]) => void; - publishGoals: (goals: GoalDefinition[]) => void; - publishState: [boolean, any]; + awaitPublish: boolean; + handlePublish: () => void; }; function AchievementPreview(props: AchievementPreviewProps) { - const { publishAchievements, publishGoals, publishState } = props; + const { awaitPublish, handlePublish } = props; const inferencer = useContext(AchievementContext); - const achievements = inferencer.getAllAchievement(); - const goals = inferencer.getAllGoalDefinition(); - - const [awaitPublish, setAwaitPublish] = publishState; - const handlePublish = () => { - // NOTE: Update goals first because goals must exist before their ID can be specified in achievements - publishGoals(goals); - publishAchievements(achievements); - setAwaitPublish(false); - }; // Show AchievementView when viewMode is true, otherwise show AchievementTask const [viewMode, setViewMode] = useState(false); diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index e1a877bf91..5207654616 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -38,13 +38,22 @@ function AchievementControl(props: DispatchProps & StateProps) { } }, [handleGetAchievements, handleGetOwnGoals]); + const achievements = inferencer.getAllAchievement(); + const goals = inferencer.getAllGoalDefinition(); + // TODO: /** * Monitors changes that are awaiting publish */ const publishState = useState(false); - const [, setAwaitPublish] = publishState; + const [awaitPublish, setAwaitPublish] = publishState; + const handlePublish = () => { + // NOTE: Update goals first because goals must exist before their ID can be specified in achievements + handleBulkUpdateGoals(goals); + handleBulkUpdateAchievements(achievements); + setAwaitPublish(false); + }; const requestPublish = () => setAwaitPublish(true); /** @@ -65,11 +74,7 @@ function AchievementControl(props: DispatchProps & StateProps) { return (
    - + From e180110ca7ef563bcd75fe2c34357ba93c742f86 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 12 Aug 2020 18:47:20 +0800 Subject: [PATCH 021/143] Move files --- .../{utils => }/AchievementCard.tsx | 6 ++--- .../achievement/AchievementOverview.tsx | 2 +- src/commons/achievement/AchievementTask.tsx | 2 +- .../{utils => card}/AchievementDeadline.tsx | 2 +- .../{utils => card}/AchievementExp.tsx | 0 .../achievement/control/AchievementEditor.tsx | 4 +-- .../EditableAchievementCard.tsx | 24 ++++++++--------- .../AchievementAdder.tsx | 2 +- .../AchievementDeleter.tsx | 0 .../AchievementSaver.tsx | 0 .../AchievementTemplate.tsx | 0 .../EditableAchievementAbility.tsx | 3 +-- .../EditableAchievementDate.tsx | 0 .../EditableAchievementExp.tsx | 0 .../EditableAchievementTitle.tsx | 8 +++--- .../EditableAchievementView.tsx | 15 ++++------- .../EditableOptions.tsx} | 26 +++++++++--------- .../EditableAchievementGoal.tsx | 0 .../EditableAchievementGoals.tsx | 0 .../{utils => overview}/AchievementLevel.tsx | 0 .../AchievementMilestone.tsx | 0 .../utils/AchievementInferencer.ts | 6 ++++- .../utils/__tests__/AchievementExp.tsx | 24 ----------------- .../utils/__tests__/AchievementLevel.tsx | 14 ---------- .../__snapshots__/AchievementExp.tsx.snap | 27 ------------------- .../__snapshots__/AchievementLevel.tsx.snap | 27 ------------------- .../__tests__/AchievementViewCompletion.tsx | 15 ----------- .../view/__tests__/AchievementViewGoal.tsx | 14 ---------- .../AchievementViewCompletion.tsx.snap | 14 ---------- .../AchievementViewGoal.tsx.snap | 3 --- 30 files changed, 49 insertions(+), 189 deletions(-) rename src/commons/achievement/{utils => }/AchievementCard.tsx (92%) rename src/commons/achievement/{utils => card}/AchievementDeadline.tsx (93%) rename src/commons/achievement/{utils => card}/AchievementExp.tsx (100%) rename src/commons/achievement/control/{editorTools => }/EditableAchievementCard.tsx (86%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/AchievementAdder.tsx (92%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/AchievementDeleter.tsx (100%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/AchievementSaver.tsx (100%) rename src/commons/achievement/control/{editorTools => achievementEditor}/AchievementTemplate.tsx (100%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/EditableAchievementAbility.tsx (91%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/EditableAchievementDate.tsx (100%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/EditableAchievementExp.tsx (100%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/EditableAchievementTitle.tsx (69%) rename src/commons/achievement/control/{editorTools/editableUtils => achievementEditor}/EditableAchievementView.tsx (80%) rename src/commons/achievement/control/{editorTools/editableUtils/EditableTools.tsx => achievementEditor/EditableOptions.tsx} (79%) rename src/commons/achievement/control/{editorTools/editableUtils/goals => goalEditor}/EditableAchievementGoal.tsx (100%) rename src/commons/achievement/control/{editorTools/editableUtils/goals => goalEditor}/EditableAchievementGoals.tsx (100%) rename src/commons/achievement/{utils => overview}/AchievementLevel.tsx (100%) rename src/commons/achievement/{utils => overview}/AchievementMilestone.tsx (100%) delete mode 100644 src/commons/achievement/utils/__tests__/AchievementExp.tsx delete mode 100644 src/commons/achievement/utils/__tests__/AchievementLevel.tsx delete mode 100644 src/commons/achievement/utils/__tests__/__snapshots__/AchievementExp.tsx.snap delete mode 100644 src/commons/achievement/utils/__tests__/__snapshots__/AchievementLevel.tsx.snap delete mode 100644 src/commons/achievement/view/__tests__/AchievementViewCompletion.tsx delete mode 100644 src/commons/achievement/view/__tests__/AchievementViewGoal.tsx delete mode 100644 src/commons/achievement/view/__tests__/__snapshots__/AchievementViewCompletion.tsx.snap delete mode 100644 src/commons/achievement/view/__tests__/__snapshots__/AchievementViewGoal.tsx.snap diff --git a/src/commons/achievement/utils/AchievementCard.tsx b/src/commons/achievement/AchievementCard.tsx similarity index 92% rename from src/commons/achievement/utils/AchievementCard.tsx rename to src/commons/achievement/AchievementCard.tsx index 3f87e98f65..8086e4ea36 100644 --- a/src/commons/achievement/utils/AchievementCard.tsx +++ b/src/commons/achievement/AchievementCard.tsx @@ -3,9 +3,9 @@ import { IconNames } from '@blueprintjs/icons'; import React, { useContext } from 'react'; import { AchievementContext, handleGlow } from 'src/features/achievement/AchievementConstants'; -import { AchievementStatus } from '../../../features/achievement/AchievementTypes'; -import AchievementDeadline from './AchievementDeadline'; -import AchievementExp from './AchievementExp'; +import { AchievementStatus } from '../../features/achievement/AchievementTypes'; +import AchievementDeadline from './card/AchievementDeadline'; +import AchievementExp from './card/AchievementExp'; type AchievementCardProps = { id: number; diff --git a/src/commons/achievement/AchievementOverview.tsx b/src/commons/achievement/AchievementOverview.tsx index 265db8155f..2eaa2065b1 100644 --- a/src/commons/achievement/AchievementOverview.tsx +++ b/src/commons/achievement/AchievementOverview.tsx @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; -import AchievementLevel from './utils/AchievementLevel'; +import AchievementLevel from './overview/AchievementLevel'; type AchievementOverviewProps = { name: string; diff --git a/src/commons/achievement/AchievementTask.tsx b/src/commons/achievement/AchievementTask.tsx index 402364643e..0261a06a0a 100644 --- a/src/commons/achievement/AchievementTask.tsx +++ b/src/commons/achievement/AchievementTask.tsx @@ -5,7 +5,7 @@ import { getAbilityColor } from '../../features/achievement/AchievementConstants'; import { AchievementStatus, FilterStatus } from '../../features/achievement/AchievementTypes'; -import AchievementCard from './utils/AchievementCard'; +import AchievementCard from './AchievementCard'; type AchievementTaskProps = { id: number; diff --git a/src/commons/achievement/utils/AchievementDeadline.tsx b/src/commons/achievement/card/AchievementDeadline.tsx similarity index 93% rename from src/commons/achievement/utils/AchievementDeadline.tsx rename to src/commons/achievement/card/AchievementDeadline.tsx index b281a57f69..2801ca04e6 100644 --- a/src/commons/achievement/utils/AchievementDeadline.tsx +++ b/src/commons/achievement/card/AchievementDeadline.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { DeadlineColors } from '../../../features/achievement/AchievementConstants'; import { AchievementAbility } from '../../../features/achievement/AchievementTypes'; -import { isExpired, prettifyDeadline, timeFromExpired } from './DateHelper'; +import { isExpired, prettifyDeadline, timeFromExpired } from '../utils/DateHelper'; type AchievementDeadlineProps = { deadline?: Date; diff --git a/src/commons/achievement/utils/AchievementExp.tsx b/src/commons/achievement/card/AchievementExp.tsx similarity index 100% rename from src/commons/achievement/utils/AchievementExp.tsx rename to src/commons/achievement/card/AchievementExp.tsx diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index a465c1be9d..72fc11d4ea 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -1,8 +1,8 @@ import React, { useContext, useState } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; -import EditableAchievementCard from './editorTools/EditableAchievementCard'; -import AchievementAdder from './editorTools/editableUtils/AchievementAdder'; +import AchievementAdder from './achievementEditor/AchievementAdder'; +import EditableAchievementCard from './EditableAchievementCard'; type AchievementEditorProps = { forceRender: () => void; diff --git a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx b/src/commons/achievement/control/EditableAchievementCard.tsx similarity index 86% rename from src/commons/achievement/control/editorTools/EditableAchievementCard.tsx rename to src/commons/achievement/control/EditableAchievementCard.tsx index bb686f0e5d..71a6aec4b1 100644 --- a/src/commons/achievement/control/editorTools/EditableAchievementCard.tsx +++ b/src/commons/achievement/control/EditableAchievementCard.tsx @@ -1,19 +1,19 @@ import { cloneDeep } from 'lodash'; import React, { useContext, useState } from 'react'; -import { AchievementContext } from '../../../../features/achievement/AchievementConstants'; +import { AchievementContext } from '../../../features/achievement/AchievementConstants'; import { AchievementAbility, AchievementItem, AchievementView -} from '../../../../features/achievement/AchievementTypes'; -import AchievementDeleter from './editableUtils/AchievementDeleter'; -import AchievementSaver from './editableUtils/AchievementSaver'; -import EditableAchievementAbility from './editableUtils/EditableAchievementAbility'; -import EditableAchievementDate from './editableUtils/EditableAchievementDate'; -import EditableAchievementTitle from './editableUtils/EditableAchievementTitle'; -import EditableAchievementView from './editableUtils/EditableAchievementView'; -import EditableTools from './editableUtils/EditableTools'; +} from '../../../features/achievement/AchievementTypes'; +import AchievementDeleter from './achievementEditor/AchievementDeleter'; +import AchievementSaver from './achievementEditor/AchievementSaver'; +import EditableAchievementAbility from './achievementEditor/EditableAchievementAbility'; +import EditableAchievementDate from './achievementEditor/EditableAchievementDate'; +import EditableAchievementTitle from './achievementEditor/EditableAchievementTitle'; +import EditableAchievementView from './achievementEditor/EditableAchievementView'; +import EditableOptions from './achievementEditor/EditableOptions'; type EditableAchievementCardProps = { id: number; @@ -137,7 +137,7 @@ function EditableAchievementCard(props: EditableAchievementCardProps) { >
    - diff --git a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx similarity index 92% rename from src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx rename to src/commons/achievement/control/achievementEditor/AchievementAdder.tsx index d06e3cdee4..4ad3aeb4e0 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/AchievementAdder.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx @@ -2,7 +2,7 @@ import { Button } from '@blueprintjs/core'; import React, { useContext } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; -import { achievementTemplate } from '../AchievementTemplate'; +import { achievementTemplate } from './AchievementTemplate'; type AchievementAdderProps = { allowNewId: boolean; diff --git a/src/commons/achievement/control/editorTools/editableUtils/AchievementDeleter.tsx b/src/commons/achievement/control/achievementEditor/AchievementDeleter.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/editableUtils/AchievementDeleter.tsx rename to src/commons/achievement/control/achievementEditor/AchievementDeleter.tsx diff --git a/src/commons/achievement/control/editorTools/editableUtils/AchievementSaver.tsx b/src/commons/achievement/control/achievementEditor/AchievementSaver.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/editableUtils/AchievementSaver.tsx rename to src/commons/achievement/control/achievementEditor/AchievementSaver.tsx diff --git a/src/commons/achievement/control/editorTools/AchievementTemplate.tsx b/src/commons/achievement/control/achievementEditor/AchievementTemplate.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/AchievementTemplate.tsx rename to src/commons/achievement/control/achievementEditor/AchievementTemplate.tsx diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementAbility.tsx b/src/commons/achievement/control/achievementEditor/EditableAchievementAbility.tsx similarity index 91% rename from src/commons/achievement/control/editorTools/editableUtils/EditableAchievementAbility.tsx rename to src/commons/achievement/control/achievementEditor/EditableAchievementAbility.tsx index ded5abf3ef..c3655df686 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementAbility.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableAchievementAbility.tsx @@ -1,8 +1,7 @@ import { Button, MenuItem } from '@blueprintjs/core'; import { ItemRenderer, Select } from '@blueprintjs/select'; import React from 'react'; - -import { AchievementAbility } from '../../../../../features/achievement/AchievementTypes'; +import { AchievementAbility } from 'src/features/achievement/AchievementTypes'; type EditableAchievementAbilityProps = { ability: AchievementAbility; diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementDate.tsx b/src/commons/achievement/control/achievementEditor/EditableAchievementDate.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/editableUtils/EditableAchievementDate.tsx rename to src/commons/achievement/control/achievementEditor/EditableAchievementDate.tsx diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementExp.tsx b/src/commons/achievement/control/achievementEditor/EditableAchievementExp.tsx similarity index 100% rename from src/commons/achievement/control/editorTools/editableUtils/EditableAchievementExp.tsx rename to src/commons/achievement/control/achievementEditor/EditableAchievementExp.tsx diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementTitle.tsx b/src/commons/achievement/control/achievementEditor/EditableAchievementTitle.tsx similarity index 69% rename from src/commons/achievement/control/editorTools/editableUtils/EditableAchievementTitle.tsx rename to src/commons/achievement/control/achievementEditor/EditableAchievementTitle.tsx index e030ab1f55..1f77d1e68d 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementTitle.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableAchievementTitle.tsx @@ -10,11 +10,9 @@ function EditableAchievementTitle(props: EditableAchievementTitleProps) { const { title, changeTitle } = props; return ( -
    -

    - -

    -
    +

    + +

    ); } diff --git a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx b/src/commons/achievement/control/achievementEditor/EditableAchievementView.tsx similarity index 80% rename from src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx rename to src/commons/achievement/control/achievementEditor/EditableAchievementView.tsx index 3485e0ce41..cbcea761d8 100644 --- a/src/commons/achievement/control/editorTools/editableUtils/EditableAchievementView.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableAchievementView.tsx @@ -1,7 +1,6 @@ import { Button, Dialog, EditableText } from '@blueprintjs/core'; import React, { useState } from 'react'; - -import { AchievementView } from '../../../../../features/achievement/AchievementTypes'; +import { AchievementView } from 'src/features/achievement/AchievementTypes'; type EditableAchievementViewProps = { view: AchievementView; @@ -16,24 +15,20 @@ function EditableAchievementView(props: EditableAchievementViewProps) { const { canvasUrl, description, completionText } = view; - const changeCanvasUrl = (canvasUrl: string) => { - changeView({ ...view, canvasUrl: canvasUrl }); - }; + const changeCanvasUrl = (canvasUrl: string) => changeView({ ...view, canvasUrl: canvasUrl }); - const changeDescription = (description: string) => { + const changeDescription = (description: string) => changeView({ ...view, description: description }); - }; - const changeCompletionText = (completionText: string) => { + const changeCompletionText = (completionText: string) => changeView({ ...view, completionText: completionText }); - }; return (
    diff --git a/src/commons/achievement/control/achievementEditor/editableOptions/EditablePosition.tsx b/src/commons/achievement/control/achievementEditor/editableOptions/EditablePosition.tsx new file mode 100644 index 0000000000..c884317df9 --- /dev/null +++ b/src/commons/achievement/control/achievementEditor/editableOptions/EditablePosition.tsx @@ -0,0 +1,35 @@ +import { Button, MenuItem } from '@blueprintjs/core'; +import { ItemRenderer, Select } from '@blueprintjs/select'; +import React, { useContext } from 'react'; +import { AchievementContext } from 'src/features/achievement/AchievementConstants'; + +type EditablePositionProps = { + changePosition: (position: number) => void; + position: number; +}; + +function EditablePosition(props: EditablePositionProps) { + const { changePosition, position } = props; + + const inferencer = useContext(AchievementContext); + const maxPosition = inferencer.listTaskIds().length + 1; + const positionOptions = [...Array(maxPosition + 1).keys()]; // maxPosition + 1 to include 0 + + const PositionSelect = Select.ofType(); + const positionRenderer: ItemRenderer = (position, { handleClick }) => ( + + ); + + return ( + +
    ); } -export default AchievementDeleter; +export default ItemDeleter; diff --git a/src/commons/achievement/control/achievementEditor/AchievementSaver.tsx b/src/commons/achievement/control/ItemSaver.tsx similarity index 79% rename from src/commons/achievement/control/achievementEditor/AchievementSaver.tsx rename to src/commons/achievement/control/ItemSaver.tsx index d432d9a718..6316acc77d 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSaver.tsx +++ b/src/commons/achievement/control/ItemSaver.tsx @@ -2,12 +2,12 @@ import { Button } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React from 'react'; -type AchievementSaverProps = { +type ItemSaverProps = { discardChanges: any; saveChanges: any; }; -function AchievementSaver(props: AchievementSaverProps) { +function ItemSaver(props: ItemSaverProps) { const { discardChanges, saveChanges } = props; return ( @@ -18,4 +18,4 @@ function AchievementSaver(props: AchievementSaverProps) { ); } -export default AchievementSaver; +export default ItemSaver; diff --git a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx index 4ad3aeb4e0..183f20ec85 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx @@ -20,7 +20,7 @@ function AchievementAdder(props: AchievementAdderProps) {
    - ); -} -*/ -export default EditableAchievementGoal; diff --git a/src/commons/achievement/control/goalEditor/EditableAchievementGoals.tsx b/src/commons/achievement/control/goalEditor/EditableAchievementGoals.tsx deleted file mode 100644 index 70b14224e1..0000000000 --- a/src/commons/achievement/control/goalEditor/EditableAchievementGoals.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -function EditableAchievementGoals() { - return <>; -} - -/* -import { AchievementGoal } from '../../../../../../features/achievement/AchievementTypes'; -import EditableAchievementGoal from './EditableAchievementGoal'; - -type EditableAchievementGoalsProps = { - goalIds: AchievementGoal[]; - editGoals: any; - removeGoalFromBackend: any; -}; - -function EditableAchievementGoals(props: EditableAchievementGoalsProps) { - const { goalIds, editGoals, removeGoalFromBackend } = props; - - const [isDialogOpen, setDialogOpen] = useState(false); - - /** - * Note: every goal's position is always matched based on its goalId. - * - * For example, a goal with goalId 0 will be the first goal - * in this array of new goals. - */ -/* - const [newGoals, setNewGoals] = useState(goalIds); - - const createNewGoal = () => { - const newID = newGoals.length; - const newGoal: AchievementGoal = { - goalId: newID, - goalText: 'Sample Text', - goalProgress: 0, - goalTarget: 0 - }; - - newGoals.push(newGoal); - setNewGoals(newGoals); - editGoals(newGoals); - }; - - const editGoal = (goal: AchievementGoal) => { - newGoals[goal.goalId] = goal; - setNewGoals(newGoals); - editGoals(newGoals); - }; - - const removeGoal = (goal: AchievementGoal) => { - newGoals.splice(goal.goalId, 1); - for (let id = 0; id < newGoals.length; id++) { - newGoals[id].goalId = id; - } - - removeGoalFromBackend(goal); - setNewGoals(newGoals); - editGoals(newGoals); - }; - - const newGoalAdder = () => { - return ( - + <> + + + - + ); } diff --git a/src/commons/achievement/control/achievementEditor/EditableOptions.tsx b/src/commons/achievement/control/achievementEditor/EditableSettings.tsx similarity index 79% rename from src/commons/achievement/control/achievementEditor/EditableOptions.tsx rename to src/commons/achievement/control/achievementEditor/EditableSettings.tsx index 2476fc4560..e518e18a21 100644 --- a/src/commons/achievement/control/achievementEditor/EditableOptions.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableSettings.tsx @@ -1,4 +1,5 @@ -import { Button, Dialog, EditableText } from '@blueprintjs/core'; +import { Button, Dialog, EditableText, Tooltip } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import React, { useContext, useState } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; @@ -6,7 +7,7 @@ import EditableGoalIds from './editableOptions/EditableGoalIds'; import EditablePosition from './editableOptions/EditablePosition'; import EditablePrerequisiteIds from './editableOptions/EditablePrerequisiteIds'; -type EditableOptionsProps = { +type EditableSettingsProps = { id: number; cardBackground: string; changeCardBackground: (cardBackground: string) => void; @@ -18,7 +19,7 @@ type EditableOptionsProps = { prerequisiteIds: number[]; }; -function EditableOptions(props: EditableOptionsProps) { +function EditableSettings(props: EditableSettingsProps) { const { id, cardBackground, @@ -37,10 +38,12 @@ function EditableOptions(props: EditableOptionsProps) { const toggleOpen = () => setOpen(!isOpen); return ( -
    -
    + ); } -export default EditableOptions; +export default EditableSettings; diff --git a/src/commons/achievement/control/achievementEditor/EditableView.tsx b/src/commons/achievement/control/achievementEditor/EditableView.tsx index 6b85206828..fbb157afcb 100644 --- a/src/commons/achievement/control/achievementEditor/EditableView.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableView.tsx @@ -1,7 +1,7 @@ -import { Button, Dialog, EditableText } from '@blueprintjs/core'; +import { Button, Dialog, EditableText, Tooltip } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; import { AchievementView } from 'src/features/achievement/AchievementTypes'; - type EditableViewProps = { view: AchievementView; changeView: any; @@ -24,10 +24,12 @@ function EditableView(props: EditableViewProps) { changeView({ ...view, completionText: completionText }); return ( -
    -
    + ); } diff --git a/src/commons/achievement/control/common/ItemDeleter.tsx b/src/commons/achievement/control/common/ItemDeleter.tsx index 14cea75621..317861fa00 100644 --- a/src/commons/achievement/control/common/ItemDeleter.tsx +++ b/src/commons/achievement/control/common/ItemDeleter.tsx @@ -1,4 +1,4 @@ -import { Button, Dialog } from '@blueprintjs/core'; +import { Button, Dialog, Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; @@ -13,14 +13,16 @@ function ItemDeleter(props: ItemDeleterProps) { const toggleOpen = () => setOpen(!isOpen); return ( -
    -
    + ); } diff --git a/src/commons/achievement/control/common/ItemSaver.tsx b/src/commons/achievement/control/common/ItemSaver.tsx index 094a023c35..bc9984ab8d 100644 --- a/src/commons/achievement/control/common/ItemSaver.tsx +++ b/src/commons/achievement/control/common/ItemSaver.tsx @@ -1,4 +1,4 @@ -import { Button } from '@blueprintjs/core'; +import { Button, Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React from 'react'; @@ -11,10 +11,15 @@ function ItemSaver(props: ItemSaverProps) { const { discardChanges, saveChanges } = props; return ( -
    -
    + <> + + From a6548bd503c9df2f7cc5b31f6e69a3f89b955056 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 15 Aug 2020 14:12:26 +0800 Subject: [PATCH 040/143] Use Collapse in task --- src/commons/achievement/AchievementTask.tsx | 5 +++-- .../achievementEditor/EditableCard.tsx | 12 ++++++++---- .../achievementEditor/EditableTitle.tsx | 19 ------------------- src/styles/_achievementcontrol.scss | 9 +++------ src/styles/_achievementdashboard.scss | 2 +- 5 files changed, 15 insertions(+), 32 deletions(-) delete mode 100644 src/commons/achievement/control/achievementEditor/EditableTitle.tsx diff --git a/src/commons/achievement/AchievementTask.tsx b/src/commons/achievement/AchievementTask.tsx index 476d7483f6..eda9d1f4ed 100644 --- a/src/commons/achievement/AchievementTask.tsx +++ b/src/commons/achievement/AchievementTask.tsx @@ -1,3 +1,4 @@ +import { Collapse } from '@blueprintjs/core'; import React, { useContext, useState } from 'react'; import { @@ -73,7 +74,7 @@ function AchievementTask(props: AchievementTaskProps) { shouldRender={shouldRender(id)} toggleDropdown={toggleDropdown} /> - {isDropdownOpen && ( +
    {prerequisiteIds.map(prerequisiteId => (
    @@ -92,7 +93,7 @@ function AchievementTask(props: AchievementTaskProps) {
    ))}
    - )} +
    )} diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 8ee70f618c..139d67ba71 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -1,3 +1,4 @@ +import { EditableText } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; import React, { useContext, useState } from 'react'; @@ -12,7 +13,6 @@ import ItemSaver from '../common/ItemSaver'; import AchievementSettings from './AchievementSettings'; import EditableAbility from './EditableAbility'; import EditableDate from './EditableDate'; -import EditableTitle from './EditableTitle'; import EditableView from './EditableView'; type EditableCardProps = { @@ -157,9 +157,13 @@ function EditableCard(props: EditableCardProps) {
    -
    - -
    +

    + +

    diff --git a/src/commons/achievement/control/achievementEditor/EditableTitle.tsx b/src/commons/achievement/control/achievementEditor/EditableTitle.tsx deleted file mode 100644 index e88bed36bd..0000000000 --- a/src/commons/achievement/control/achievementEditor/EditableTitle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { EditableText } from '@blueprintjs/core'; -import React from 'react'; - -type EditableTitleProps = { - title: string; - changeTitle: any; -}; - -function EditableTitle(props: EditableTitleProps) { - const { title, changeTitle } = props; - - return ( -

    - -

    - ); -} - -export default EditableTitle; diff --git a/src/styles/_achievementcontrol.scss b/src/styles/_achievementcontrol.scss index 7ecd0c169c..f419051983 100644 --- a/src/styles/_achievementcontrol.scss +++ b/src/styles/_achievementcontrol.scss @@ -94,6 +94,7 @@ display: flex; flex-direction: column; height: $canvas-height; + text-align: center; width: $canvas-width; h1 { @@ -110,7 +111,6 @@ color: yellow; font-size: 0.85em; margin: auto auto 1em; - text-align: center; width: 80%; } } @@ -305,16 +305,13 @@ display: flex; flex-direction: column; height: $card-height; + justify-content: space-evenly; width: $card-width * 0.64; .title { align-items: center; display: flex; - height: $card-height * 0.5; - - h3 { - max-width: 100%; - } + margin: 0; } .details { diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index a8375c6da2..98c6da2f79 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -318,6 +318,7 @@ display: flex; flex-direction: column; height: $canvas-height; + text-align: center; width: $canvas-width; h1 { @@ -334,7 +335,6 @@ color: yellow; font-size: 0.85em; margin: auto auto 1em; - text-align: center; width: 80%; } } From b76d4b5ba6092e82fc6c8a981a0b3b63cdae9b24 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 16 Aug 2020 23:50:02 +0800 Subject: [PATCH 041/143] Add prompt in achievementControl --- src/pages/achievement/control/AchievementControl.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index d09d195b3f..514f590dd7 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Prompt } from 'react-router'; import AchievementEditor from '../../../commons/achievement/control/AchievementEditor'; import AchievementPreview from '../../../commons/achievement/control/AchievementPreview'; @@ -41,8 +42,6 @@ function AchievementControl(props: DispatchProps & StateProps) { const achievements = inferencer.getAllAchievements(); const goals = inferencer.getAllGoals(); - // TODO: - /** * Monitors changes that are awaiting publish */ @@ -76,6 +75,11 @@ function AchievementControl(props: DispatchProps & StateProps) { return ( + +
    From 9f0df05ae7db32b229470b3f97e4d1c0f8dbdb9b Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 00:30:26 +0800 Subject: [PATCH 042/143] Restore milestone --- .../achievement/overview/AchievementLevel.tsx | 4 ++-- .../overview/AchievementMilestone.tsx | 3 --- src/styles/_achievementdashboard.scss | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/commons/achievement/overview/AchievementLevel.tsx b/src/commons/achievement/overview/AchievementLevel.tsx index f3a21acfa1..88a67cee4e 100644 --- a/src/commons/achievement/overview/AchievementLevel.tsx +++ b/src/commons/achievement/overview/AchievementLevel.tsx @@ -20,8 +20,8 @@ function AchievementLevel(props: AchievementLevelProps) { const progressFrac = progress / expPerLevel; return ( -
    -
    +
    +

    {level}

    diff --git a/src/commons/achievement/overview/AchievementMilestone.tsx b/src/commons/achievement/overview/AchievementMilestone.tsx index 8a502e527a..3364b4a2af 100644 --- a/src/commons/achievement/overview/AchievementMilestone.tsx +++ b/src/commons/achievement/overview/AchievementMilestone.tsx @@ -3,7 +3,6 @@ import React from 'react'; type AchievementMilestoneProps = {}; function AchievementMilestone(props: AchievementMilestoneProps) { - /* TODO: Implement Milestone return (

    ACHIEVEMENT LEVEL

    @@ -23,8 +22,6 @@ function AchievementMilestone(props: AchievementMilestoneProps) {
    ); - */ - return <>; } export default AchievementMilestone; diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index 98c6da2f79..cd529f2e7d 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -67,20 +67,19 @@ } } - /* Temporarily removed .milestone { - background-color: rgba(0, 0, 0, 0.7); - border: 1px solid #90ff12; - box-shadow: 0 0 10px #90ff12; + background-color: rgba(0, 0, 0, 0.9); + border: 1px solid yellow; + box-shadow: 0 0 10px yellow; display: flex; flex-direction: column; - margin: 20em 0 0 1em; - padding: 1em 2em; + margin: 20em 0 0 0; + padding: 1em 2.5em; position: absolute; z-index: 2; h2 { - margin: 0; + margin: $default-spacing; text-align: center; text-decoration: underline; } @@ -89,12 +88,14 @@ align-items: center; display: flex; flex-direction: row; + margin: $default-spacing; .description { - padding: 1em 0 0 0; + margin: 0; + padding: 0 0 0 $default-spacing; } } - } */ + } } .level, From 7149efd14aa30b7b825a729dd3ada6e19b25c031 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 00:58:46 +0800 Subject: [PATCH 043/143] Rename exp to xp, assessmentId to assessmentNumber --- src/commons/achievement/AchievementCard.tsx | 6 +- .../achievement/AchievementOverview.tsx | 4 +- src/commons/achievement/AchievementView.tsx | 4 +- .../achievement/card/AchievementExp.tsx | 29 -------- .../achievement/card/AchievementXp.tsx | 29 ++++++++ .../control/goalEditor/GoalTemplate.tsx | 2 +- .../achievement/overview/AchievementLevel.tsx | 14 ++-- .../utils/AchievementInferencer.ts | 36 +++++----- .../view/AchievementViewCompletion.tsx | 6 +- .../achievement/view/AchievementViewGoal.tsx | 4 +- src/commons/mocks/AchievementMocks.ts | 66 +++++++++---------- .../achievement/AchievementConstants.ts | 2 +- src/features/achievement/AchievementTypes.ts | 19 +++--- src/styles/_achievementcontrol.scss | 2 +- src/styles/_achievementdashboard.scss | 2 +- 15 files changed, 112 insertions(+), 113 deletions(-) delete mode 100644 src/commons/achievement/card/AchievementExp.tsx create mode 100644 src/commons/achievement/card/AchievementXp.tsx diff --git a/src/commons/achievement/AchievementCard.tsx b/src/commons/achievement/AchievementCard.tsx index 524d54c391..3d44ae1b84 100644 --- a/src/commons/achievement/AchievementCard.tsx +++ b/src/commons/achievement/AchievementCard.tsx @@ -5,7 +5,7 @@ import { AchievementContext, handleGlow } from 'src/features/achievement/Achieve import { AchievementStatus } from '../../features/achievement/AchievementTypes'; import AchievementDeadline from './card/AchievementDeadline'; -import AchievementExp from './card/AchievementExp'; +import AchievementXp from './card/AchievementXp'; type AchievementCardProps = { id: number; @@ -24,7 +24,7 @@ function AchievementCard(props: AchievementCardProps) { const { ability, cardTileUrl, title } = inferencer.getAchievement(id); const displayDeadline = inferencer.getDisplayDeadline(id); - const displayExp = inferencer.getAchievementMaxExp(id); + const displayXp = inferencer.getAchievementMaxXp(id); const progressFrac = inferencer.getProgressFrac(id); const status = inferencer.getStatus(id); @@ -62,7 +62,7 @@ function AchievementCard(props: AchievementCardProps) {

    {ability}

    - +
    - +

    {name}

    {studio}

    diff --git a/src/commons/achievement/AchievementView.tsx b/src/commons/achievement/AchievementView.tsx index 67b30f5886..18b4b29e7f 100644 --- a/src/commons/achievement/AchievementView.tsx +++ b/src/commons/achievement/AchievementView.tsx @@ -33,7 +33,7 @@ function AchievementView(props: AchievementViewProps) { const achievement = inferencer.getAchievement(focusId); const { ability, deadline, title, view } = achievement; const { canvasUrl, completionText, description } = view; - const awardedExp = inferencer.getAchievementExp(focusId); + const awardedXp = inferencer.getAchievementXp(focusId); const goals = inferencer.listGoals(focusId); const prereqGoals = inferencer.listPrerequisiteGoals(focusId); const status = inferencer.getStatus(focusId); @@ -62,7 +62,7 @@ function AchievementView(props: AchievementViewProps) { {status === AchievementStatus.COMPLETED && ( <>
    - + )}
    diff --git a/src/commons/achievement/card/AchievementExp.tsx b/src/commons/achievement/card/AchievementExp.tsx deleted file mode 100644 index 713931f847..0000000000 --- a/src/commons/achievement/card/AchievementExp.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Icon } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import React from 'react'; - -type AchievementExpProps = { - exp: number; - isBonus: boolean; -}; - -const stringifyExp = (exp: number, isBonus: boolean) => { - return (isBonus ? '+' : '') + exp + ' XP'; -}; - -function AchievementExp(props: AchievementExpProps) { - const { exp, isBonus } = props; - - return ( -
    - {exp !== 0 && ( - <> - -

    {stringifyExp(exp, isBonus)}

    - - )} -
    - ); -} - -export default AchievementExp; diff --git a/src/commons/achievement/card/AchievementXp.tsx b/src/commons/achievement/card/AchievementXp.tsx new file mode 100644 index 0000000000..991143fd1c --- /dev/null +++ b/src/commons/achievement/card/AchievementXp.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; + +type AchievementXpProps = { + xp: number; + isBonus: boolean; +}; + +const stringifyXp = (xp: number, isBonus: boolean) => { + return (isBonus ? '+' : '') + xp + ' XP'; +}; + +function AchievementXp(props: AchievementXpProps) { + const { xp, isBonus } = props; + + return ( +
    + {xp !== 0 && ( + <> + +

    {stringifyXp(xp, isBonus)}

    + + )} +
    + ); +} + +export default AchievementXp; diff --git a/src/commons/achievement/control/goalEditor/GoalTemplate.tsx b/src/commons/achievement/control/goalEditor/GoalTemplate.tsx index 852d507e02..012ed012c6 100644 --- a/src/commons/achievement/control/goalEditor/GoalTemplate.tsx +++ b/src/commons/achievement/control/goalEditor/GoalTemplate.tsx @@ -5,6 +5,6 @@ export const goalTemplate: GoalDefinition = { text: 'Goal Text Here', meta: { type: GoalType.MANUAL, - maxExp: 0 + maxXp: 0 } }; diff --git a/src/commons/achievement/overview/AchievementLevel.tsx b/src/commons/achievement/overview/AchievementLevel.tsx index 88a67cee4e..ff65605766 100644 --- a/src/commons/achievement/overview/AchievementLevel.tsx +++ b/src/commons/achievement/overview/AchievementLevel.tsx @@ -1,23 +1,23 @@ import { ProgressBar } from '@blueprintjs/core'; import React, { useState } from 'react'; -import { expPerLevel } from '../../../features/achievement/AchievementConstants'; +import { xpPerLevel } from '../../../features/achievement/AchievementConstants'; import AchievementMilestone from './AchievementMilestone'; type AchievementLevelProps = { - studentExp: number; + studentXp: number; }; function AchievementLevel(props: AchievementLevelProps) { - const { studentExp } = props; + const { studentXp } = props; const [showMilestone, setShowMilestone] = useState(false); const displayMilestone = () => setShowMilestone(true); const hideMilestone = () => setShowMilestone(false); - const level = Math.floor(studentExp / expPerLevel); - const progress = studentExp % expPerLevel; - const progressFrac = progress / expPerLevel; + const level = Math.floor(studentXp / xpPerLevel); + const progress = studentXp % xpPerLevel; + const progressFrac = progress / xpPerLevel; return (
    @@ -33,7 +33,7 @@ function AchievementLevel(props: AchievementLevelProps) { stripes={false} />

    - {progress} / {expPerLevel} XP + {progress} / {xpPerLevel} XP

    {showMilestone && } diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 67ee1f23dc..2b6ccc9a1e 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -12,7 +12,7 @@ import { isExpired } from './DateHelper'; * * @param {AchievementItem} achievement the achievement item * @param {Date | undefined} displayDeadline deadline displayed on the achievement card - * @param {number} maxExp total achievable EXP of the achievement + * @param {number} maxXp maximum attainable XP of the achievement * @param {number} progressFrac progress percentage in fraction. It is always between 0 to 1, both inclusive. * @param {AchievementStatus} status the achievement status * @param {Set} children a set of immediate prerequisites id @@ -21,7 +21,7 @@ import { isExpired } from './DateHelper'; class AchievementNode { public achievement: AchievementItem; public displayDeadline?: Date; - public maxExp: number; + public maxXp: number; public progressFrac: number; public status: AchievementStatus; public children: Set; @@ -32,7 +32,7 @@ class AchievementNode { this.achievement = achievement; this.displayDeadline = deadline; - this.maxExp = 0; + this.maxXp = 0; this.progressFrac = 0; this.status = AchievementStatus.ACTIVE; this.children = new Set(prerequisiteIds); @@ -299,29 +299,29 @@ class AchievementInferencer { } /** - * Returns EXP earned from the achievement + * Returns XP earned from the achievement * * @param id Achievement Id */ - public getAchievementExp(id: number) { + public getAchievementXp(id: number) { const { goalIds } = this.nodeList.get(id)!.achievement; - return goalIds.reduce((exp, goalId) => exp + this.goalList.get(goalId)!.exp, 0); + return goalIds.reduce((xp, goalId) => xp + this.goalList.get(goalId)!.xp, 0); } /** - * Returns the maximum attainable EXP from the achievement + * Returns the maximum attainable XP from the achievement * * @param id Achievement Id */ - public getAchievementMaxExp(id: number) { - return this.nodeList.get(id)!.maxExp; + public getAchievementMaxXp(id: number) { + return this.nodeList.get(id)!.maxXp; } /** - * Returns total EXP earned from all goals + * Returns total XP earned from all goals */ - public getTotalExp() { - return this.getAllGoals().reduce((totalExp, goal) => totalExp + goal.exp, 0); + public getTotalXp() { + return this.getAllGoals().reduce((totalXp, goal) => totalXp + goal.xp, 0); } /** @@ -407,7 +407,7 @@ class AchievementInferencer { this.nodeList.forEach(node => { this.generateDescendant(node); this.generateDisplayDeadline(node); - this.generateMaxExp(node); + this.generateMaxXp(node); this.generateProgressFrac(node); this.generateStatus(node); @@ -504,13 +504,13 @@ class AchievementInferencer { } /** - * Calculates the achievement maximum attainable EXP + * Calculates the achievement maximum attainable XP * * @param node the AchievementNode */ - private generateMaxExp(node: AchievementNode) { + private generateMaxXp(node: AchievementNode) { const { goalIds } = node.achievement; - node.maxExp = goalIds.reduce((maxExp, goalId) => maxExp + this.goalList.get(goalId)!.maxExp, 0); + node.maxXp = goalIds.reduce((maxXp, goalId) => maxXp + this.goalList.get(goalId)!.maxXp, 0); } /** @@ -520,9 +520,9 @@ class AchievementInferencer { */ private generateProgressFrac(node: AchievementNode) { const { goalIds } = node.achievement; - const exp = goalIds.reduce((exp, goalId) => exp + this.goalList.get(goalId)!.exp, 0); + const xp = goalIds.reduce((xp, goalId) => xp + this.goalList.get(goalId)!.xp, 0); - node.progressFrac = node.maxExp === 0 ? 0 : Math.min(exp / node.maxExp, 1); + node.progressFrac = node.maxXp === 0 ? 0 : Math.min(xp / node.maxXp, 1); } /** diff --git a/src/commons/achievement/view/AchievementViewCompletion.tsx b/src/commons/achievement/view/AchievementViewCompletion.tsx index 05f10e1c91..1e6796df61 100644 --- a/src/commons/achievement/view/AchievementViewCompletion.tsx +++ b/src/commons/achievement/view/AchievementViewCompletion.tsx @@ -1,16 +1,16 @@ import React from 'react'; type AchievementViewCompletionProps = { - awardedExp: number; + awardedXp: number; completionText: string; }; function AchievementViewCompletion(props: AchievementViewCompletionProps) { - const { awardedExp, completionText } = props; + const { awardedXp, completionText } = props; return (
    -

    {`AWARDED ${awardedExp}XP`}

    +

    {`AWARDED ${awardedXp}XP`}

    {completionText}

    ); diff --git a/src/commons/achievement/view/AchievementViewGoal.tsx b/src/commons/achievement/view/AchievementViewGoal.tsx index 89dc3abea5..ccf8311bf9 100644 --- a/src/commons/achievement/view/AchievementViewGoal.tsx +++ b/src/commons/achievement/view/AchievementViewGoal.tsx @@ -12,13 +12,13 @@ type AchievementViewGoalProps = { * @param goal an array of goalId */ const mapGoalToJSX = (goal: AchievementGoal) => { - const { id, text, maxExp, exp } = goal; + const { id, text, maxXp, xp } = goal; return (

    - {exp} / {maxExp} XP + {xp} / {maxXp} XP

    {text}

    diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index 83517babbb..016ecf2b74 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -237,10 +237,10 @@ export const mockGoals: AchievementGoal[] = [ { event: EventTypes.ASSESSMENT_GRADING, restriction: 'M2A' }, { event: EventTypes.ASSESSMENT_GRADING, restriction: 'M2B' } ), - maxExp: 100 + maxXp: 100 }, - exp: 0, - maxExp: 100, + xp: 0, + maxXp: 100, completed: false }, { @@ -248,11 +248,11 @@ export const mockGoals: AchievementGoal[] = [ text: 'XP earned from Beyond the Second Dimension Mission', meta: { type: GoalType.ASSESSMENT, - assessmentId: '5', + assessmentNumber: 'M2B', requiredCompletionFrac: 0.5 }, - exp: 213, - maxExp: 250, + xp: 213, + maxXp: 250, completed: true }, { @@ -260,11 +260,11 @@ export const mockGoals: AchievementGoal[] = [ text: 'XP earned from Colorful Carpet Mission', meta: { type: GoalType.ASSESSMENT, - assessmentId: '3', + assessmentNumber: 'M2A', requiredCompletionFrac: 0.8 }, - exp: 0, - maxExp: 250, + xp: 0, + maxXp: 250, completed: false }, { @@ -282,10 +282,10 @@ export const mockGoals: AchievementGoal[] = [ restriction: 'M4A' } ), - maxExp: 100 + maxXp: 100 }, - exp: 0, - maxExp: 100, + xp: 0, + maxXp: 100, completed: false }, { @@ -293,11 +293,11 @@ export const mockGoals: AchievementGoal[] = [ text: 'XP earned from Curve Introduction Mission', meta: { type: GoalType.ASSESSMENT, - assessmentId: '7', + assessmentNumber: 'M3', requiredCompletionFrac: 150 }, - exp: 178, - maxExp: 250, + xp: 178, + maxXp: 250, completed: true }, { @@ -305,11 +305,11 @@ export const mockGoals: AchievementGoal[] = [ text: 'XP earned from Curve Manipulation Mission', meta: { type: GoalType.ASSESSMENT, - assessmentId: '8', + assessmentNumber: 'M4A', requiredCompletionFrac: 0.8 }, - exp: 191, - maxExp: 250, + xp: 191, + maxXp: 250, completed: false }, { @@ -321,10 +321,10 @@ export const mockGoals: AchievementGoal[] = [ event: EventTypes.ASSESSMENT_SUBMISSION, restriction: 'P3' }, - maxExp: 100 + maxXp: 100 }, - exp: 100, - maxExp: 100, + xp: 100, + maxXp: 100, completed: true }, { @@ -332,11 +332,11 @@ export const mockGoals: AchievementGoal[] = [ text: 'XP earned from Source 3 Path', meta: { type: GoalType.ASSESSMENT, - assessmentId: '12', + assessmentNumber: 'P3', requiredCompletionFrac: 1 }, - exp: 300, - maxExp: 300, + xp: 300, + maxXp: 300, completed: true }, { @@ -344,10 +344,10 @@ export const mockGoals: AchievementGoal[] = [ text: 'Each Top Voted answer in Piazza gives 10 XP', meta: { type: GoalType.MANUAL, - maxExp: 100 + maxXp: 100 }, - exp: 40, - maxExp: 100, + xp: 40, + maxXp: 100, completed: false }, { @@ -355,10 +355,10 @@ export const mockGoals: AchievementGoal[] = [ text: 'Submit 1 PR to Source Academy Github', meta: { type: GoalType.MANUAL, - maxExp: 100 + maxXp: 100 }, - exp: 100, - maxExp: 100, + xp: 100, + maxXp: 100, completed: true }, { @@ -367,10 +367,10 @@ export const mockGoals: AchievementGoal[] = [ meta: { type: GoalType.BINARY, condition: false, - maxExp: 100 + maxXp: 100 }, - exp: 0, - maxExp: 100, + xp: 0, + maxXp: 100, completed: false } ]; diff --git a/src/features/achievement/AchievementConstants.ts b/src/features/achievement/AchievementConstants.ts index cac474b144..5e25e1a1cc 100644 --- a/src/features/achievement/AchievementConstants.ts +++ b/src/features/achievement/AchievementConstants.ts @@ -5,7 +5,7 @@ import { defaultAchievement } from '../../commons/application/ApplicationTypes'; import { Links } from '../../commons/utils/Constants'; import { AchievementAbility, FilterStatus } from './AchievementTypes'; -export const expPerLevel = 1000; +export const xpPerLevel = 1000; const { achievements: defaultAchievements, goals: defaultGoals } = defaultAchievement; export const AchievementContext = React.createContext( diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 2f620761e3..4e2d3cf173 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -79,23 +79,22 @@ export type GoalDefinition = { /** * Information of an achievement goal progress - * NOTE: Achievement EXP named to deconflict with Assessment XP * * @param {number} id unique id of the goal - * @param {number} exp student's current exp of the goal - * @param {number} maxExp maximum attainable exp of the goal (computed by server) + * @param {number} xp student's current XP of the goal + * @param {number} maxXp maximum attainable XP of the goal (computed by server) * @param {boolean} completed student's completion status of the goal */ export type GoalProgress = { id: number; - exp: number; - maxExp: number; + xp: number; + maxXp: number; completed: boolean; }; export const defaultGoalProgress = { - exp: 0, - maxExp: 0, + xp: 0, + maxXp: 0, completed: false }; @@ -109,19 +108,19 @@ export type GoalMeta = AssessmentMeta | BinaryMeta | ManualMeta; export type AssessmentMeta = { type: GoalType.ASSESSMENT; - assessmentId: string; // e.g. 'M1A', 'P2' + assessmentNumber: string; // e.g. 'M1A', 'P2' requiredCompletionFrac: number; // between [0..1] }; export type BinaryMeta = { type: GoalType.BINARY; condition: BooleanExpression; - maxExp: number; + maxXp: number; }; export type ManualMeta = { type: GoalType.MANUAL; - maxExp: number; + maxXp: number; }; /** diff --git a/src/styles/_achievementcontrol.scss b/src/styles/_achievementcontrol.scss index f419051983..3bdee3855d 100644 --- a/src/styles/_achievementcontrol.scss +++ b/src/styles/_achievementcontrol.scss @@ -217,7 +217,7 @@ } .deadline, - .exp { + .xp { align-items: center; display: flex; flex: 1 1 25%; diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index cd529f2e7d..d23dadc023 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -223,7 +223,7 @@ } .deadline, - .exp { + .xp { align-items: center; display: flex; flex: 1 1 25%; From 37a141f78a80ac4adbf4a31140912c6f3ee147f3 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 02:11:44 +0800 Subject: [PATCH 044/143] Rename cardTileUrl, canvasUrl - cardTileUrl -> cardBackground - canvasUrl -> coverImage --- src/commons/achievement/AchievementCard.tsx | 4 +- src/commons/achievement/AchievementView.tsx | 6 +- .../achievementEditor/AchievementSettings.tsx | 2 +- .../achievementEditor/AchievementTemplate.tsx | 13 ++- .../achievementEditor/EditableCard.tsx | 10 +- .../achievementEditor/EditableView.tsx | 12 +-- .../utils/__tests__/Inferencer.test.ts | 8 +- src/commons/mocks/AchievementMocks.ts | 92 +++++++++---------- .../achievement/AchievementConstants.ts | 16 ++-- src/features/achievement/AchievementTypes.ts | 8 +- src/styles/_achievementcontrol.scss | 20 ++-- src/styles/_achievementdashboard.scss | 20 ++-- 12 files changed, 107 insertions(+), 104 deletions(-) diff --git a/src/commons/achievement/AchievementCard.tsx b/src/commons/achievement/AchievementCard.tsx index 3d44ae1b84..d1d01c574e 100644 --- a/src/commons/achievement/AchievementCard.tsx +++ b/src/commons/achievement/AchievementCard.tsx @@ -22,7 +22,7 @@ function AchievementCard(props: AchievementCardProps) { const [focusId, setFocusId] = focusState; - const { ability, cardTileUrl, title } = inferencer.getAchievement(id); + const { ability, cardBackground, title } = inferencer.getAchievement(id); const displayDeadline = inferencer.getDisplayDeadline(id); const displayXp = inferencer.getAchievementMaxXp(id); const progressFrac = inferencer.getProgressFrac(id); @@ -38,7 +38,7 @@ function AchievementCard(props: AchievementCardProps) { style={{ ...handleGlow(id, focusId, ability), opacity: shouldRender ? '100%' : '20%', - background: `url(${cardTileUrl}) center/cover` + background: `url(${cardBackground}) center/cover` }} onClick={() => setFocusId(id)} onClickCapture={toggleDropdown} diff --git a/src/commons/achievement/AchievementView.tsx b/src/commons/achievement/AchievementView.tsx index 18b4b29e7f..04470e9f1e 100644 --- a/src/commons/achievement/AchievementView.tsx +++ b/src/commons/achievement/AchievementView.tsx @@ -32,7 +32,7 @@ function AchievementView(props: AchievementViewProps) { const achievement = inferencer.getAchievement(focusId); const { ability, deadline, title, view } = achievement; - const { canvasUrl, completionText, description } = view; + const { coverImage, completionText, description } = view; const awardedXp = inferencer.getAchievementXp(focusId); const goals = inferencer.listGoals(focusId); const prereqGoals = inferencer.listPrerequisiteGoals(focusId); @@ -41,9 +41,9 @@ function AchievementView(props: AchievementViewProps) { return (

    {title.toUpperCase()}

    diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index 716ec54d44..7cf57762f5 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -47,7 +47,7 @@ function AchievementSettings(props: AchievementSettingsProps) {

    Card Background

    setEditableAchievement(cloneDeep(achievementReference)); const { ability, - cardTileUrl, + cardBackground, deadline, goalIds, position, @@ -75,10 +75,10 @@ function EditableCard(props: EditableCardProps) { setIsDirty(true); }; - const handleChangeCardBackground = (cardTileUrl: string) => { + const handleChangeCardBackground = (cardBackground: string) => { setEditableAchievement({ ...editableAchievement, - cardTileUrl: cardTileUrl + cardBackground: cardBackground }); setIsDirty(true); }; @@ -145,7 +145,7 @@ function EditableCard(props: EditableCardProps) {
  • @@ -175,7 +175,7 @@ function EditableCard(props: EditableCardProps) { (false); const toggleOpen = () => setOpen(!isOpen); - const { canvasUrl, description, completionText } = view; + const { coverImage, description, completionText } = view; - const changeCanvasUrl = (canvasUrl: string) => changeView({ ...view, canvasUrl: canvasUrl }); + const changeCoverImage = (coverImage: string) => changeView({ ...view, coverImage }); const changeDescription = (description: string) => changeView({ ...view, description: description }); @@ -31,12 +31,12 @@ function EditableView(props: EditableViewProps) {
    -

    View Image

    +

    Cover Image

    Description

    { switch (ability) { case AchievementAbility.CORE: return { - background: `url(${backgroundUrl}/core-background.png) no-repeat center/cover` + background: `url(${backgroundUrl}/core.png) no-repeat center/cover` }; case AchievementAbility.EFFORT: return { - background: `url(${backgroundUrl}/effort-background.png) no-repeat center/cover` + background: `url(${backgroundUrl}/effort.png) no-repeat center/cover` }; case AchievementAbility.EXPLORATION: return { - background: `url(${backgroundUrl}/exploration-background.png) no-repeat center/cover` + background: `url(${backgroundUrl}/exploration.png) no-repeat center/cover` }; case AchievementAbility.COMMUNITY: return { - background: `url(${backgroundUrl}/community-background.png) no-repeat center/cover` + background: `url(${backgroundUrl}/community.png) no-repeat center/cover` }; case AchievementAbility.FLEX: return { - background: `url(${backgroundUrl}/flex-background.png) no-repeat center/cover` + background: `url(${backgroundUrl}/flex.png) no-repeat center/cover` }; default: return { diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 4e2d3cf173..982af6328f 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -45,7 +45,7 @@ export enum FilterStatus { * @param {number} position ordering of the achievement task, 0 for non-task * @param {number[]} prerequisiteIds an array of prerequisite ids * @param {number[]} goalIds an array of goal ids - * @param {string} cardTileUrl background image URL of the achievement card + * @param {string} cardBackground background image URL of the achievement card * @param {AchievementView} view the achievement view */ export type AchievementItem = { @@ -58,7 +58,7 @@ export type AchievementItem = { position: number; prerequisiteIds: number[]; goalIds: number[]; - cardTileUrl: string; + cardBackground: string; view: AchievementView; }; @@ -126,12 +126,12 @@ export type ManualMeta = { /** * Information of an achievement view * - * @param {string} canvasUrl canvas image URL + * @param {string} coverImage cover image URL * @param {string} description fixed text that displays under title * @param {string} completionText text that displays after student completes the achievement */ export type AchievementView = { - canvasUrl: string; + coverImage: string; description: string; completionText: string; }; diff --git a/src/styles/_achievementcontrol.scss b/src/styles/_achievementcontrol.scss index 3bdee3855d..03fbc61378 100644 --- a/src/styles/_achievementcontrol.scss +++ b/src/styles/_achievementcontrol.scss @@ -1,15 +1,15 @@ .AchievementControl { - // Canvas aspect ratio 2:1 - $canvas-height: 18em; - $canvas-width: 36em; + // Cover aspect ratio 2:1 + $cover-height: 18em; + $cover-width: 36em; // Card aspect ratio 2:1 $card-height: 5em; $card-width: 30em; // View aspect ratio 18:25 $view-height: 50em; - $view-width: $canvas-width; + $view-width: $cover-width; - $canvas-spacing: 0.3em; + $cover-spacing: 0.3em; $content-spacing: 0.5em; $default-spacing: 1em; @@ -90,21 +90,21 @@ min-height: $view-height; width: $view-width; - .canvas { + .cover { display: flex; flex-direction: column; - height: $canvas-height; + height: $cover-height; text-align: center; - width: $canvas-width; + width: $cover-width; h1 { margin: 0.5em auto 0; - padding: $canvas-spacing; + padding: $cover-spacing; } p { margin: 0 auto; - padding: $canvas-spacing; + padding: $cover-spacing; } .description { diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index d23dadc023..cf9f7ed463 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -106,9 +106,9 @@ .achievement-main { $border-glow-radius: 10px; - // Canvas aspect ratio 2:1 - $canvas-height: 18em; - $canvas-width: 36em; + // Cover aspect ratio 2:1 + $cover-height: 18em; + $cover-width: 36em; // Card aspect ratio 2:1 $card-height: 5em; $card-width: 30em; @@ -116,7 +116,7 @@ $default-spacing: 1em; // View aspect ratio 18:25 $view-height: 50em; - $view-width: $canvas-width; + $view-width: $cover-width; align-items: center; display: flex; @@ -285,7 +285,7 @@ } .view-container { - $canvas-spacing: 0.3em; + $cover-spacing: 0.3em; $content-spacing: 0.5em; @extend %container; @@ -315,21 +315,21 @@ min-height: $view-height; width: $view-width; - .canvas { + .cover { display: flex; flex-direction: column; - height: $canvas-height; + height: $cover-height; text-align: center; - width: $canvas-width; + width: $cover-width; h1 { margin: 0.5em auto 0; - padding: $canvas-spacing; + padding: $cover-spacing; } p { margin: 0 auto; - padding: $canvas-spacing; + padding: $cover-spacing; } .description { From f483e6ba71e20afd149d0ac356e5ab48e8573554 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 02:50:39 +0800 Subject: [PATCH 045/143] Update delete item prompt text --- .../control/achievementEditor/EditableCard.tsx | 2 +- src/commons/achievement/control/common/ItemDeleter.tsx | 9 +++++---- .../achievement/control/goalEditor/EditableGoal.tsx | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 3ba92ac62c..4790574497 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -152,7 +152,7 @@ function EditableCard(props: EditableCardProps) { {isDirty ? ( ) : ( - + )}
    diff --git a/src/commons/achievement/control/common/ItemDeleter.tsx b/src/commons/achievement/control/common/ItemDeleter.tsx index 3eabd30811..d09fcb0cf0 100644 --- a/src/commons/achievement/control/common/ItemDeleter.tsx +++ b/src/commons/achievement/control/common/ItemDeleter.tsx @@ -4,21 +4,22 @@ import React from 'react'; import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; type ItemDeleterProps = { - deleteItem: () => void; + item: string; + handleDelete: () => void; }; function ItemDeleter(props: ItemDeleterProps) { - const { deleteItem } = props; + const { item, handleDelete } = props; const handleConfirmDelete = async () => { const confirm = await showSimpleConfirmDialog({ - contents: 'Are you sure you want to delete?', + contents: `Are you sure you want to delete '${item}' ?`, positiveIntent: 'danger', positiveLabel: 'Yes, delete', negativeLabel: 'No' }); if (confirm) { - deleteItem(); + handleDelete(); } }; diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index cdacf5f138..9b4b589389 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -72,7 +72,7 @@ function EditableGoal(props: EditableGoalProps) { {isDirty ? ( ) : ( - + )}

    From 6ac8f269e4333bf5f0a0ae799ea60d5ac964b63c Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 05:03:13 +0800 Subject: [PATCH 046/143] Add error logging in inferencer --- .../utils/AchievementInferencer.ts | 61 ++++++++++++++----- src/commons/utils/NotificationsHelper.ts | 14 +++++ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 2b6ccc9a1e..4a7c43257d 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -1,3 +1,6 @@ +import assert from 'assert'; + +import { showDangerMessage } from '../../../commons/utils/NotificationsHelper'; import { AchievementGoal, AchievementItem, @@ -92,15 +95,27 @@ class AchievementInferencer { * @param id Achievement Id */ public getAchievement(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.achievement; } + /** + * Returns the AchievementGoal + * + * @param id Goal Id + */ + public getGoal(id: number) { + assert(this.goalList.has(id), `goal ${id} not found`); + return this.goalList.get(id)!; + } + /** * Returns the GoalDefinition * * @param id Goal Id */ public getGoalDefinition(id: number) { + assert(this.goalList.has(id), `goal definition ${id} not found`); return this.goalList.get(id)! as GoalDefinition; } @@ -261,8 +276,8 @@ class AchievementInferencer { * @param id Achievement Id */ public listGoals(id: number) { - const { goalIds } = this.nodeList.get(id)!.achievement; - return goalIds.map(goalId => this.goalList.get(goalId)!); + const { goalIds } = this.getAchievement(id); + return goalIds.map(goalId => this.getGoal(goalId)); } /** @@ -272,12 +287,12 @@ class AchievementInferencer { */ public listPrerequisiteGoals(id: number) { const childGoalIds: number[] = []; - for (const childId of this.nodeList.get(id)!.children) { - const { goalIds } = this.nodeList.get(childId)!.achievement; + for (const childId of this.getImmediateChildren(id)) { + const { goalIds } = this.getAchievement(childId); goalIds.forEach(goalId => childGoalIds.push(goalId)); } - return childGoalIds.map(goalId => this.goalList.get(goalId)!); + return childGoalIds.map(goalId => this.getGoal(goalId)); } /** @@ -286,6 +301,7 @@ class AchievementInferencer { * @param text goalText */ public getIdByText(text: string) { + assert(this.textToId.has(text), `Goal ${text} not found`); return this.textToId.get(text)!; } @@ -295,6 +311,7 @@ class AchievementInferencer { * @param title achievementTitle */ public getIdByTitle(title: string) { + assert(this.titleToId.has(title), `Achievement ${title} not found`); return this.titleToId.get(title)!; } @@ -304,8 +321,8 @@ class AchievementInferencer { * @param id Achievement Id */ public getAchievementXp(id: number) { - const { goalIds } = this.nodeList.get(id)!.achievement; - return goalIds.reduce((xp, goalId) => xp + this.goalList.get(goalId)!.xp, 0); + const { goalIds } = this.getAchievement(id); + return goalIds.reduce((xp, goalId) => xp + this.getGoal(goalId).xp, 0); } /** @@ -314,6 +331,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getAchievementMaxXp(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.maxXp; } @@ -330,6 +348,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getProgressFrac(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.progressFrac; } @@ -339,6 +358,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getStatus(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.status; } @@ -348,6 +368,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getDisplayDeadline(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.displayDeadline; } @@ -358,6 +379,7 @@ class AchievementInferencer { * @param childId Child Achievement Id */ public isImmediateChild(id: number, childId: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.children.has(childId); } @@ -367,6 +389,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getImmediateChildren(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.children; } @@ -377,6 +400,7 @@ class AchievementInferencer { * @param childId Descendant Achievement Id */ public isDescendant(id: number, childId: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.descendant.has(childId); } @@ -386,6 +410,7 @@ class AchievementInferencer { * @param id Achievement Id */ public getDescendants(id: number) { + assert(this.nodeList.has(id), `achievement ${id} not found`); return this.nodeList.get(id)!.descendant; } @@ -404,6 +429,8 @@ class AchievementInferencer { * Recalculates the AchievementNode data of each achievements, O(N) operation */ private processNodes() { + this.titleToId = new Map(); + this.nodeList.forEach(node => { this.generateDescendant(node); this.generateDisplayDeadline(node); @@ -417,6 +444,8 @@ class AchievementInferencer { } private processGoals() { + this.textToId = new Map(); + this.goalList.forEach(goal => { const { text, id } = goal; this.textToId.set(text, id); @@ -455,9 +484,13 @@ class AchievementInferencer { private generateDescendant(node: AchievementNode) { for (const childId of node.descendant) { if (childId === node.achievement.id) { - console.error('Circular dependency detected'); + const { title } = node.achievement; + // NOTE: not the best error handling practice, but as long as admin verifies the + // data in AchievementPreview and do not publish new achievements with circular + // dependency error, it should be suffice + showDangerMessage(`Circular dependency detected in achievement ${title}`, 30000); } - for (const grandchildId of this.nodeList.get(childId)!.descendant) { + for (const grandchildId of this.getDescendants(childId)) { // Newly added grandchild is appended to the back of the set. node.descendant.add(grandchildId); // Hence the great grandchildren will be added when the iterator reaches there @@ -495,7 +528,7 @@ class AchievementInferencer { // Temporary array of all descendants' deadlines const descendantDeadlines: (Date | undefined)[] = []; for (const childId of node.descendant) { - const childDeadline = this.nodeList.get(childId)!.achievement.deadline; + const childDeadline = this.getAchievement(childId).deadline; descendantDeadlines.push(childDeadline); } @@ -510,7 +543,7 @@ class AchievementInferencer { */ private generateMaxXp(node: AchievementNode) { const { goalIds } = node.achievement; - node.maxXp = goalIds.reduce((maxXp, goalId) => maxXp + this.goalList.get(goalId)!.maxXp, 0); + node.maxXp = goalIds.reduce((maxXp, goalId) => maxXp + this.getGoal(goalId).maxXp, 0); } /** @@ -520,7 +553,7 @@ class AchievementInferencer { */ private generateProgressFrac(node: AchievementNode) { const { goalIds } = node.achievement; - const xp = goalIds.reduce((xp, goalId) => xp + this.goalList.get(goalId)!.xp, 0); + const xp = goalIds.reduce((xp, goalId) => xp + this.getGoal(goalId).xp, 0); node.progressFrac = node.maxXp === 0 ? 0 : Math.min(xp / node.maxXp, 1); } @@ -540,12 +573,12 @@ class AchievementInferencer { const achievementCompleted = goalIds.length !== 0 && goalIds - .map(goalId => this.goalList.get(goalId)!.completed) + .map(goalId => this.getGoal(goalId).completed) .reduce((result, goalCompleted) => result && goalCompleted, true); const descendantDeadlines = []; for (const childId of node.descendant) { - const childDeadline = this.nodeList.get(childId)!.achievement.deadline; + const childDeadline = this.getAchievement(childId).deadline; descendantDeadlines.push(childDeadline); } const hasUnexpiredDeadline = descendantDeadlines.reduce( diff --git a/src/commons/utils/NotificationsHelper.ts b/src/commons/utils/NotificationsHelper.ts index 9fb749e458..973c897d80 100644 --- a/src/commons/utils/NotificationsHelper.ts +++ b/src/commons/utils/NotificationsHelper.ts @@ -32,6 +32,20 @@ export const showWarningMessage = ( key ); +export const showDangerMessage = ( + message: string | JSX.Element, + timeout: number = 2000, + key?: string +) => + notification.show( + { + intent: Intent.DANGER, + message, + timeout + }, + key + ); + export const showMessage = (props: IToastProps, key?: string) => notification.show(props, key); export const dismiss = (key: string) => notification.dismiss(key); From 0f4fee192598ee0dce900aecad88ffe16e9101bc Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 06:16:10 +0800 Subject: [PATCH 047/143] Fix delete achievement & goal bug --- .../achievementSettings/EditableGoalIds.tsx | 8 ++++-- .../EditablePrerequisiteIds.tsx | 8 ++++-- .../utils/AchievementInferencer.ts | 28 +++++++++++++++---- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx index 9b34891f10..1fef54344f 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx @@ -14,8 +14,8 @@ function EditableGoalIds(props: EditableGoalIdsProps) { const { allGoalIds, changeGoalIds, goalIds } = props; const inferencer = useContext(AchievementContext); - const getId = inferencer.getIdByText; - const getText = (id: number) => inferencer.getGoalDefinition(id).text; + const getId = (text: string) => inferencer.getIdByText(text); + const getText = (id: number) => inferencer.getTextById(id); const GoalSelect = MultiSelect.ofType(); const goalRenderer: ItemRenderer = (id, { handleClick }) => ( @@ -31,7 +31,9 @@ function EditableGoalIds(props: EditableGoalIdsProps) { changeGoalIds([...selectedGoals]); }; - const handleRemoveGoal = (removeId: number) => { + const handleRemoveGoal = (removeId?: number) => { + if (removeId === undefined) return; + selectedGoals.delete(removeId); availableGoals.add(removeId); changeGoalIds([...selectedGoals]); diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx index 07e535b3ef..1e7c8c234c 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx @@ -13,8 +13,8 @@ function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { const { availableIds, changePrerequisiteIds, prerequisiteIds } = props; const inferencer = useContext(AchievementContext); - const getId = inferencer.getIdByTitle; - const getTitle = (id: number) => inferencer.getAchievement(id).title; + const getId = (title: string) => inferencer.getIdByTitle(title); + const getTitle = (id: number) => inferencer.getTitleById(id); const PrerequisiteSelect = MultiSelect.ofType(); const prerequisiteRenderer: ItemRenderer = (id, { handleClick }) => ( @@ -30,7 +30,9 @@ function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { changePrerequisiteIds([...selectedPrereqs]); }; - const handleRemovePrereq = (removeId: number) => { + const handleRemovePrereq = (removeId?: number) => { + if (removeId === undefined) return; + selectedPrereqs.delete(removeId); availablePrereqs.add(removeId); changePrerequisiteIds([...selectedPrereqs]); diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 4a7c43257d..82e24230a7 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -296,23 +296,39 @@ class AchievementInferencer { } /** - * Returns the Goal Id associate to the Goal Text + * Returns the Goal Id associate to the Goal Text or undefined * * @param text goalText */ public getIdByText(text: string) { - assert(this.textToId.has(text), `Goal ${text} not found`); - return this.textToId.get(text)!; + return this.textToId.get(text); } /** - * Returns the Achievement Id associate to the Achievement Title + * Returns the Goal Text associate to the Goal Id or undefined + * + * @param text goalId + */ + public getTextById(id: number) { + return this.goalList.get(id)?.text; + } + + /** + * Returns the Achievement Id associate to the Achievement Title or undefined * * @param title achievementTitle */ public getIdByTitle(title: string) { - assert(this.titleToId.has(title), `Achievement ${title} not found`); - return this.titleToId.get(title)!; + return this.titleToId.get(title); + } + + /** + * Returns the Achievement Title associate to the Achievement Id or undefined + * + * @param text achievementId + */ + public getTitleById(id: number) { + return this.nodeList.get(id)?.achievement.title; } /** From f07ecd78181eaee8efefca2373e6a0f46ab1b2a3 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 06:23:55 +0800 Subject: [PATCH 048/143] Fix formatting --- .../achievementEditor/achievementSettings/EditablePosition.tsx | 2 +- .../achievementSettings/EditablePrerequisiteIds.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePosition.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePosition.tsx index c884317df9..072498ce9a 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePosition.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePosition.tsx @@ -13,7 +13,7 @@ function EditablePosition(props: EditablePositionProps) { const inferencer = useContext(AchievementContext); const maxPosition = inferencer.listTaskIds().length + 1; - const positionOptions = [...Array(maxPosition + 1).keys()]; // maxPosition + 1 to include 0 + const positionOptions = [...Array(maxPosition + 1).keys()]; // [0..maxPosition + 1] const PositionSelect = Select.ofType(); const positionRenderer: ItemRenderer = (position, { handleClick }) => ( diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx index 1e7c8c234c..ee2a31819f 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx @@ -32,7 +32,7 @@ function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { const handleRemovePrereq = (removeId?: number) => { if (removeId === undefined) return; - + selectedPrereqs.delete(removeId); availablePrereqs.add(removeId); changePrerequisiteIds([...selectedPrereqs]); From 133fdf0d43252be3d6ec53ec71d07a5e5f6e7ec4 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 06:43:24 +0800 Subject: [PATCH 049/143] Restrict prop types --- .../achievement/control/achievementEditor/EditableAbility.tsx | 2 +- .../achievement/control/achievementEditor/EditableCard.tsx | 4 ++-- .../achievement/control/achievementEditor/EditableDate.tsx | 2 +- .../achievement/control/achievementEditor/EditableView.tsx | 2 +- src/commons/achievement/control/common/ItemSaver.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/EditableAbility.tsx b/src/commons/achievement/control/achievementEditor/EditableAbility.tsx index 0edf40746c..fd73d0a70f 100644 --- a/src/commons/achievement/control/achievementEditor/EditableAbility.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableAbility.tsx @@ -5,7 +5,7 @@ import { AchievementAbility } from 'src/features/achievement/AchievementTypes'; type EditableAbilityProps = { ability: AchievementAbility; - changeAbility: any; + changeAbility: (ability: AchievementAbility) => void; }; function EditableAbility(props: EditableAbilityProps) { diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 4790574497..fd83ee554b 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -83,7 +83,7 @@ function EditableCard(props: EditableCardProps) { setIsDirty(true); }; - const handleChangeDeadline = (deadline: Date) => { + const handleChangeDeadline = (deadline?: Date) => { setEditableAchievement({ ...editableAchievement, deadline: deadline @@ -117,7 +117,7 @@ function EditableCard(props: EditableCardProps) { setIsDirty(true); }; - const handleChangeRelease = (release: Date) => { + const handleChangeRelease = (release?: Date) => { setEditableAchievement({ ...editableAchievement, release: release diff --git a/src/commons/achievement/control/achievementEditor/EditableDate.tsx b/src/commons/achievement/control/achievementEditor/EditableDate.tsx index fa50a185e0..59309e22d9 100644 --- a/src/commons/achievement/control/achievementEditor/EditableDate.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableDate.tsx @@ -6,7 +6,7 @@ import { prettifyDate } from 'src/commons/achievement/utils/DateHelper'; type EditableDateProps = { type: string; date?: Date; - changeDate: any; + changeDate: (date?: Date) => void; }; function EditableDate(props: EditableDateProps) { diff --git a/src/commons/achievement/control/achievementEditor/EditableView.tsx b/src/commons/achievement/control/achievementEditor/EditableView.tsx index fdcd58fb93..4016d11dfa 100644 --- a/src/commons/achievement/control/achievementEditor/EditableView.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableView.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { AchievementView } from 'src/features/achievement/AchievementTypes'; type EditableViewProps = { view: AchievementView; - changeView: any; + changeView: (view: AchievementView) => void; }; function EditableView(props: EditableViewProps) { diff --git a/src/commons/achievement/control/common/ItemSaver.tsx b/src/commons/achievement/control/common/ItemSaver.tsx index 094aba82bf..06479e181b 100644 --- a/src/commons/achievement/control/common/ItemSaver.tsx +++ b/src/commons/achievement/control/common/ItemSaver.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { showSuccessMessage, showWarningMessage } from 'src/commons/utils/NotificationsHelper'; type ItemSaverProps = { - discardChanges: any; - saveChanges: any; + discardChanges: () => void; + saveChanges: () => void; }; function ItemSaver(props: ItemSaverProps) { From cb9d150eb922af5ace42cee999a9083fba3b2bbc Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 07:11:58 +0800 Subject: [PATCH 050/143] Implement goal meta editor --- .../control/goalEditor/EditableMeta.tsx | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/commons/achievement/control/goalEditor/EditableMeta.tsx b/src/commons/achievement/control/goalEditor/EditableMeta.tsx index ab4c959a04..a348fa5b10 100644 --- a/src/commons/achievement/control/goalEditor/EditableMeta.tsx +++ b/src/commons/achievement/control/goalEditor/EditableMeta.tsx @@ -1,7 +1,8 @@ -import { Button, MenuItem } from '@blueprintjs/core'; -import { ItemRenderer, Select } from '@blueprintjs/select'; +import { EditableText } from '@blueprintjs/core'; import React, { useState } from 'react'; -import { GoalMeta, GoalType } from 'src/features/achievement/AchievementTypes'; +import { GoalMeta } from 'src/features/achievement/AchievementTypes'; + +import ItemSaver from '../common/ItemSaver'; type EditableMetaProps = { meta: GoalMeta; @@ -9,24 +10,41 @@ type EditableMetaProps = { }; function EditableMeta(props: EditableMetaProps) { - const { meta } = props; + const { meta, changeMeta } = props; - const [goalType, setGoalType] = useState(meta.type); + const [editableMeta, setEditableMeta] = useState(JSON.stringify(meta)); + const resetEditableMeta = () => setEditableMeta(JSON.stringify(meta)); - const GoalTypeSelect = Select.ofType(); - const goalTypeRenderer: ItemRenderer = (type, { handleClick }) => ( - - ); + const [isDirty, setIsDirty] = useState(false); + + const handleChangeMeta = (metaString: string) => { + setEditableMeta(metaString); + setIsDirty(true); + }; + + const handleSaveChanges = () => { + const parsedMeta = JSON.parse(editableMeta); + changeMeta(parsedMeta); + setIsDirty(false); + }; + + const handleDiscardChanges = () => { + resetEditableMeta(); + setIsDirty(false); + }; return ( - -

  • diff --git a/src/commons/achievement/AchievementFilter.tsx b/src/commons/achievement/AchievementFilter.tsx index 0ecdc2c854..8d728e088c 100644 --- a/src/commons/achievement/AchievementFilter.tsx +++ b/src/commons/achievement/AchievementFilter.tsx @@ -5,13 +5,13 @@ import { getFilterColor } from '../../features/achievement/AchievementConstants' import { FilterStatus } from '../../features/achievement/AchievementTypes'; type AchievementFilterProps = { - ownStatus: FilterStatus; - icon: IconName; filterState: [FilterStatus, any]; + icon: IconName; + ownStatus: FilterStatus; }; function AchievementFilter(props: AchievementFilterProps) { - const { ownStatus, icon, filterState } = props; + const { filterState, icon, ownStatus } = props; const [globalStatus, setGlobalStatus] = filterState; @@ -21,7 +21,7 @@ function AchievementFilter(props: AchievementFilterProps) { onClick={() => setGlobalStatus(ownStatus)} style={{ color: getFilterColor(globalStatus, ownStatus) }} > - +

    {ownStatus}

    ); diff --git a/src/commons/achievement/AchievementTask.tsx b/src/commons/achievement/AchievementTask.tsx index eda9d1f4ed..8038fbead1 100644 --- a/src/commons/achievement/AchievementTask.tsx +++ b/src/commons/achievement/AchievementTask.tsx @@ -18,13 +18,12 @@ function AchievementTask(props: AchievementTaskProps) { const { id, filterStatus, focusState } = props; const inferencer = useContext(AchievementContext); + const prerequisiteIds = [...inferencer.getImmediateChildren(id)]; + const taskColor = getAbilityColor(inferencer.getAchievement(id).ability); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen); - const prerequisiteIds = [...inferencer.getImmediateChildren(id)]; - const taskColor = getAbilityColor(inferencer.getAchievement(id).ability); - /** * Checks whether the AchievementItem (can be a task or prereq) should be rendered * based on the achievement dashboard filterStatus. diff --git a/src/commons/achievement/card/AchievementDeadline.tsx b/src/commons/achievement/card/AchievementDeadline.tsx index a2c364fa5b..612667d11b 100644 --- a/src/commons/achievement/card/AchievementDeadline.tsx +++ b/src/commons/achievement/card/AchievementDeadline.tsx @@ -7,14 +7,14 @@ import { AchievementAbility } from '../../../features/achievement/AchievementTyp import { isExpired, prettifyDeadline, timeFromExpired } from '../utils/DateHelper'; type AchievementDeadlineProps = { - deadline?: Date; ability: AchievementAbility; + deadline?: Date; }; const twoDays = new Date(0, 0, 2).getTime() - new Date(0, 0, 0).getTime(); function AchievementDeadline(props: AchievementDeadlineProps) { - const { deadline, ability } = props; + const { ability, deadline } = props; // red deadline color for core achievements that are expiring in less than 2 days const deadlineColor = @@ -27,7 +27,7 @@ function AchievementDeadline(props: AchievementDeadlineProps) { return (
    - +

    {prettifyDeadline(deadline)}

    ); diff --git a/src/commons/achievement/card/AchievementXp.tsx b/src/commons/achievement/card/AchievementXp.tsx index 991143fd1c..c35a150ccd 100644 --- a/src/commons/achievement/card/AchievementXp.tsx +++ b/src/commons/achievement/card/AchievementXp.tsx @@ -3,8 +3,8 @@ import { IconNames } from '@blueprintjs/icons'; import React from 'react'; type AchievementXpProps = { - xp: number; isBonus: boolean; + xp: number; }; const stringifyXp = (xp: number, isBonus: boolean) => { @@ -12,7 +12,7 @@ const stringifyXp = (xp: number, isBonus: boolean) => { }; function AchievementXp(props: AchievementXpProps) { - const { xp, isBonus } = props; + const { isBonus, xp } = props; return (
    diff --git a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx index 1646c286c9..128799f53f 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx @@ -20,10 +20,10 @@ function AchievementAdder(props: AchievementAdderProps) { return (

    - - + +
    - + - + - ); diff --git a/src/commons/achievement/control/achievementEditor/EditableView.tsx b/src/commons/achievement/control/achievementEditor/EditableView.tsx index 4016d11dfa..5b223745da 100644 --- a/src/commons/achievement/control/achievementEditor/EditableView.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableView.tsx @@ -3,18 +3,17 @@ import { IconNames } from '@blueprintjs/icons'; import React, { useState } from 'react'; import { AchievementView } from 'src/features/achievement/AchievementTypes'; type EditableViewProps = { - view: AchievementView; changeView: (view: AchievementView) => void; + view: AchievementView; }; function EditableView(props: EditableViewProps) { - const { view, changeView } = props; + const { changeView, view } = props; + const { coverImage, description, completionText } = view; const [isOpen, setOpen] = useState(false); const toggleOpen = () => setOpen(!isOpen); - const { coverImage, description, completionText } = view; - const changeCoverImage = (coverImage: string) => changeView({ ...view, coverImage }); const changeDescription = (description: string) => @@ -33,23 +32,23 @@ function EditableView(props: EditableViewProps) {

    Cover Image

    Description

    Completion Text

    diff --git a/src/commons/achievement/control/common/ItemDeleter.tsx b/src/commons/achievement/control/common/ItemDeleter.tsx index d09fcb0cf0..9fe675a575 100644 --- a/src/commons/achievement/control/common/ItemDeleter.tsx +++ b/src/commons/achievement/control/common/ItemDeleter.tsx @@ -4,19 +4,19 @@ import React from 'react'; import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; type ItemDeleterProps = { - item: string; handleDelete: () => void; + item: string; }; function ItemDeleter(props: ItemDeleterProps) { - const { item, handleDelete } = props; + const { handleDelete, item } = props; const handleConfirmDelete = async () => { const confirm = await showSimpleConfirmDialog({ contents: `Are you sure you want to delete '${item}' ?`, + negativeLabel: 'No', positiveIntent: 'danger', - positiveLabel: 'Yes, delete', - negativeLabel: 'No' + positiveLabel: 'Yes, delete' }); if (confirm) { handleDelete(); diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 9b4b589389..44a53bd506 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -72,14 +72,14 @@ function EditableGoal(props: EditableGoalProps) { {isDirty ? ( ) : ( - + )}

    - +

    - +
    ); diff --git a/src/commons/achievement/control/goalEditor/GoalAdder.tsx b/src/commons/achievement/control/goalEditor/GoalAdder.tsx index ba328a14cc..a8b47cd956 100644 --- a/src/commons/achievement/control/goalEditor/GoalAdder.tsx +++ b/src/commons/achievement/control/goalEditor/GoalAdder.tsx @@ -20,10 +20,10 @@ function GoalAdder(props: GoalAdderProps) { return (

    {progress} / {xpPerLevel} XP diff --git a/src/commons/achievement/overview/AchievementMilestone.tsx b/src/commons/achievement/overview/AchievementMilestone.tsx index 3364b4a2af..636163737d 100644 --- a/src/commons/achievement/overview/AchievementMilestone.tsx +++ b/src/commons/achievement/overview/AchievementMilestone.tsx @@ -1,8 +1,6 @@ import React from 'react'; -type AchievementMilestoneProps = {}; - -function AchievementMilestone(props: AchievementMilestoneProps) { +function AchievementMilestone() { return (

    ACHIEVEMENT LEVEL

    diff --git a/src/features/achievement/AchievementConstants.ts b/src/features/achievement/AchievementConstants.ts index 6aa3c74b15..c69e29d8af 100644 --- a/src/features/achievement/AchievementConstants.ts +++ b/src/features/achievement/AchievementConstants.ts @@ -5,8 +5,6 @@ import { defaultAchievement } from '../../commons/application/ApplicationTypes'; import { Links } from '../../commons/utils/Constants'; import { AchievementAbility, FilterStatus } from './AchievementTypes'; -export const xpPerLevel = 1000; - const { achievements: defaultAchievements, goals: defaultGoals } = defaultAchievement; export const AchievementContext = React.createContext( new AchievementInferencer(defaultAchievements, defaultGoals) @@ -22,8 +20,39 @@ export enum FilterColors { WHITE = '#fff' } -export const getFilterColor = (globalStatus: FilterStatus, ownStatus: FilterStatus) => - globalStatus === ownStatus ? FilterColors.BLUE : FilterColors.WHITE; +export const achievementAssets = `${Links.sourceAcademyAssets}/achievement`; +export const backgroundUrl = `${achievementAssets}/view-background`; +export const cardBackgroundUrl = `${achievementAssets}/card-background`; +export const coverImageUrl = `${achievementAssets}/cover-image`; + +export const getAbilityBackground = (ability: AchievementAbility) => { + switch (ability) { + case AchievementAbility.CORE: + return { + background: `url(${backgroundUrl}/core.png) no-repeat center/cover` + }; + case AchievementAbility.EFFORT: + return { + background: `url(${backgroundUrl}/effort.png) no-repeat center/cover` + }; + case AchievementAbility.EXPLORATION: + return { + background: `url(${backgroundUrl}/exploration.png) no-repeat center/cover` + }; + case AchievementAbility.COMMUNITY: + return { + background: `url(${backgroundUrl}/community.png) no-repeat center/cover` + }; + case AchievementAbility.FLEX: + return { + background: `url(${backgroundUrl}/flex.png) no-repeat center/cover` + }; + default: + return { + background: `` + }; + } +}; export const getAbilityColor = (ability: AchievementAbility) => { switch (ability) { @@ -56,40 +85,11 @@ export const getAbilityGlow = (ability: AchievementAbility) => boxShadow: `0 0 10px ${getAbilityColor(ability)}` }; +export const getFilterColor = (globalStatus: FilterStatus, ownStatus: FilterStatus) => + globalStatus === ownStatus ? FilterColors.BLUE : FilterColors.WHITE; + // Make selected achievements + view and Flex achievements glow export const handleGlow = (id: number, focusId: number, ability: AchievementAbility) => ability === AchievementAbility.FLEX || id === focusId ? getAbilityGlow(ability) : undefined; -export const achievementAssets = `${Links.sourceAcademyAssets}/achievement`; -export const backgroundUrl = `${achievementAssets}/view-background`; -export const cardBackgroundUrl = `${achievementAssets}/card-background`; -export const coverImageUrl = `${achievementAssets}/cover-image`; - -export const getAbilityBackground = (ability: AchievementAbility) => { - switch (ability) { - case AchievementAbility.CORE: - return { - background: `url(${backgroundUrl}/core.png) no-repeat center/cover` - }; - case AchievementAbility.EFFORT: - return { - background: `url(${backgroundUrl}/effort.png) no-repeat center/cover` - }; - case AchievementAbility.EXPLORATION: - return { - background: `url(${backgroundUrl}/exploration.png) no-repeat center/cover` - }; - case AchievementAbility.COMMUNITY: - return { - background: `url(${backgroundUrl}/community.png) no-repeat center/cover` - }; - case AchievementAbility.FLEX: - return { - background: `url(${backgroundUrl}/flex.png) no-repeat center/cover` - }; - default: - return { - background: `` - }; - } -}; +export const xpPerLevel = 1000; diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 514f590dd7..4b78598f18 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -76,8 +76,8 @@ function AchievementControl(props: DispatchProps & StateProps) { return (
    diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 01e2e50dc8..161542bf3a 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -17,9 +17,9 @@ export type DispatchProps = { }; export type StateProps = { + group: string | null; inferencer: AchievementInferencer; name?: string; - group: string | null; }; /** @@ -39,7 +39,7 @@ export const generateAchievementTasks = ( )); function Dashboard(props: DispatchProps & StateProps) { - const { inferencer, name, group, handleGetAchievements, handleGetOwnGoals } = props; + const { group, handleGetAchievements, handleGetOwnGoals, inferencer, name } = props; /** * The dashboard fetches the latest achievements and goals from backend @@ -70,19 +70,19 @@ function Dashboard(props: DispatchProps & StateProps) {
    diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 0f7b9c498f..29827a1359 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -9,11 +9,11 @@ import { getAchievements, getOwnGoals } from '../../../features/achievement/Achi import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; const mapStateToProps: MapStateToProps = state => ({ + group: state.session.group, inferencer: Constants.useBackend ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) : new AchievementInferencer(mockAchievements, mockGoals), - name: state.session.name, - group: state.session.group + name: state.session.name }); const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => From a99dfb9933bd5bf18b622ae052e07c4ba23420da Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 14:16:23 +0800 Subject: [PATCH 053/143] useReducer in editableGoal --- .../control/goalEditor/EditableGoal.tsx | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 44a53bd506..71f5bb7f75 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -1,6 +1,6 @@ import { EditableText } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; -import React, { useContext, useState } from 'react'; +import React, { useContext, useMemo, useReducer } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTypes'; @@ -14,57 +14,82 @@ type EditableGoalProps = { requestPublish: () => void; }; +const reducer = ( + state: { editableGoal: GoalDefinition; isDirty: boolean }, + action: { type: string; payload?: any } +) => { + switch (action.type) { + case 'SAVE_CHANGES': + return { + ...state, + isDirty: false + }; + case 'DISCARD_CHANGES': + return { + editableGoal: action.payload, + isDirty: false + }; + case 'DELETE_GOAL': + return { + ...state, + isDirty: false + }; + case 'CHANGE_TEXT': + return { + editableGoal: { + ...state.editableGoal, + text: action.payload + }, + isDirty: true + }; + case 'CHANGE_META': + return { + editableGoal: { + ...state.editableGoal, + meta: action.payload + }, + isDirty: true + }; + default: + return state; + } +}; + +const initialState = { + editableGoal: {} as GoalDefinition, + isDirty: false +}; + function EditableGoal(props: EditableGoalProps) { const { id, releaseId, requestPublish } = props; const inferencer = useContext(AchievementContext); - const goalReference = inferencer.getGoalDefinition(id); + const goal = inferencer.getGoalDefinition(id); + const goalClone = useMemo(() => cloneDeep(goal), [goal]); - const [editableGoal, setEditableGoal] = useState( - () => cloneDeep(goalReference) // Expensive, only clone once on initialization - ); - const resetEditableGoal = () => setEditableGoal(cloneDeep(goalReference)); + const [state, dispatch] = useReducer(reducer, { ...initialState, editableGoal: goalClone }); + const { editableGoal, isDirty } = state; const { text, meta } = editableGoal; - // A save/discard button appears on top of the card when it's dirty - const [isDirty, setIsDirty] = useState(false); + const handleDiscardChanges = () => dispatch({ type: 'DISCARD_CHANGES', payload: goalClone }); - // TODO: Replace the following 3 useState with useReducer for state management & cleanup const handleSaveChanges = () => { inferencer.modifyGoalDefinition(editableGoal); - setIsDirty(false); releaseId(id); requestPublish(); - }; - - const handleDiscardChanges = () => { - resetEditableGoal(); - setIsDirty(false); + dispatch({ type: 'SAVE_CHANGES' }); }; const handleDeleteGoal = () => { inferencer.removeGoalDefinition(id); - setIsDirty(false); releaseId(id); requestPublish(); + dispatch({ type: 'DELETE_GOAL' }); }; - // TODO: Replace all of the following useState with useReducer for editable content - const handleChangeText = (text: string) => { - setEditableGoal({ - ...editableGoal, - text: text - }); - setIsDirty(true); - }; + const handleChangeText = (text: string) => dispatch({ type: 'CHANGE_TEXT', payload: text }); - const handleChangeMeta = (meta: GoalMeta) => { - setEditableGoal({ - ...editableGoal, - meta: meta - }); - setIsDirty(true); - }; + const handleChangeMeta = (meta: GoalMeta) => dispatch({ type: 'CHANGE_META', payload: meta }); return (
  • From 82dbaf7b0cb80c54714dda8d090f114115268136 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 14:57:57 +0800 Subject: [PATCH 054/143] useReducer in editableCard --- .../achievementEditor/EditableCard.tsx | 216 +++++++++++------- 1 file changed, 133 insertions(+), 83 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 7486bc08c4..f737e6ff16 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -1,6 +1,6 @@ import { EditableText } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; -import React, { useContext, useState } from 'react'; +import React, { useContext, useMemo, useReducer } from 'react'; import { AchievementContext } from '../../../../features/achievement/AchievementConstants'; import { @@ -21,16 +21,121 @@ type EditableCardProps = { requestPublish: () => void; }; +const reducer = ( + state: { editableAchievement: AchievementItem; isDirty: boolean }, + action: { type: string; payload?: any } +) => { + switch (action.type) { + case 'SAVE_CHANGES': + return { + ...state, + isDirty: false + }; + case 'DISCARD_CHANGES': + return { + editableAchievement: action.payload, + isDirty: false + }; + case 'DELETE_ACHIEVEMENT': + return { + ...state, + isDirty: false + }; + case 'CHANGE_ABILITY': + return { + editableAchievement: { + ...state.editableAchievement, + ability: action.payload + }, + isDirty: true + }; + case 'CHANGE_CARD_BACKGROUND': + return { + editableAchievement: { + ...state.editableAchievement, + cardBackground: action.payload + }, + isDirty: true + }; + case 'CHANGE_DEADLINE': + return { + editableAchievement: { + ...state.editableAchievement, + deadline: action.payload + }, + isDirty: true + }; + case 'CHANGE_GOAL_IDS': + return { + editableAchievement: { + ...state.editableAchievement, + goalIds: action.payload + }, + isDirty: true + }; + case 'CHANGE_POSITION': + return { + editableAchievement: { + ...state.editableAchievement, + isTask: action.payload !== 0, + position: action.payload + }, + isDirty: true + }; + case 'CHANGE_PREREQUISITE_IDS': + return { + editableAchievement: { + ...state.editableAchievement, + prerequisiteIds: action.payload + }, + isDirty: true + }; + case 'CHANGE_RELEASE': + return { + editableAchievement: { + ...state.editableAchievement, + release: action.payload + }, + isDirty: true + }; + case 'CHANGE_TITLE': + return { + editableAchievement: { + ...state.editableAchievement, + title: action.payload + }, + isDirty: true + }; + case 'CHANGE_VIEW': + return { + editableAchievement: { + ...state.editableAchievement, + view: action.payload + }, + isDirty: true + }; + default: + return state; + } +}; + +const initialState = { + editableAchievement: {} as AchievementItem, + isDirty: false +}; + function EditableCard(props: EditableCardProps) { const { id, releaseId, requestPublish } = props; const inferencer = useContext(AchievementContext); - const achievementReference = inferencer.getAchievement(id); - - const [editableAchievement, setEditableAchievement] = useState( - () => cloneDeep(achievementReference) // Expensive, only clone once on initialization - ); - const resetEditableAchievement = () => setEditableAchievement(cloneDeep(achievementReference)); + const achievement = inferencer.getAchievement(id); + const achievementClone = useMemo(() => cloneDeep(achievement), [achievement]); + + const [state, dispatch] = useReducer(reducer, { + ...initialState, + editableAchievement: achievementClone + }); + const { editableAchievement, isDirty } = state; const { ability, cardBackground, @@ -43,103 +148,48 @@ function EditableCard(props: EditableCardProps) { view } = editableAchievement; - // A save/discard button appears on top of the card when it's dirty - const [isDirty, setIsDirty] = useState(false); + const handleDiscardChanges = () => + dispatch({ type: 'DISCARD_CHANGES', payload: achievementClone }); - // TODO: Replace the following 3 useState with useReducer for state management & cleanup const handleSaveChanges = () => { inferencer.modifyAchievement(editableAchievement); - setIsDirty(false); releaseId(id); requestPublish(); - }; - - const handleDiscardChanges = () => { - resetEditableAchievement(); - setIsDirty(false); + dispatch({ type: 'SAVE_CHANGES' }); }; const handleDeleteAchievement = () => { inferencer.removeAchievement(id); - setIsDirty(false); releaseId(id); requestPublish(); + dispatch({ type: 'DELETE_ACHIEVEMENT' }); }; - // TODO: Replace all of the following useState with useReducer for editable content - const handleChangeAbility = (ability: AchievementAbility) => { - setEditableAchievement({ - ...editableAchievement, - ability: ability - }); - setIsDirty(true); - }; + const handleChangeAbility = (ability: AchievementAbility) => + dispatch({ type: 'CHANGE_ABILITY', payload: ability }); - const handleChangeCardBackground = (cardBackground: string) => { - setEditableAchievement({ - ...editableAchievement, - cardBackground: cardBackground - }); - setIsDirty(true); - }; + const handleChangeCardBackground = (cardBackground: string) => + dispatch({ type: 'CHANGE_CARD_BACKGROUND', payload: cardBackground }); - const handleChangeDeadline = (deadline?: Date) => { - setEditableAchievement({ - ...editableAchievement, - deadline: deadline - }); - setIsDirty(true); - }; + const handleChangeDeadline = (deadline?: Date) => + dispatch({ type: 'CHANGE_DEADLINE', payload: deadline }); - const handleChangeGoalIds = (goalIds: number[]) => { - setEditableAchievement({ - ...editableAchievement, - goalIds: goalIds - }); - setIsDirty(true); - }; + const handleChangeGoalIds = (goalIds: number[]) => + dispatch({ type: 'CHANGE_GOAL_IDS', payload: goalIds }); - const handleChangePosition = (position: number) => { - const isTask = position !== 0; - setEditableAchievement({ - ...editableAchievement, - isTask: isTask, - position: position - }); - setIsDirty(true); - }; + const handleChangePosition = (position: number) => + dispatch({ type: 'CHANGE_POSITION', payload: position }); - const handleChangePrerequisiteIds = (prerequisiteIds: number[]) => { - setEditableAchievement({ - ...editableAchievement, - prerequisiteIds: prerequisiteIds - }); - setIsDirty(true); - }; + const handleChangePrerequisiteIds = (prerequisiteIds: number[]) => + dispatch({ type: 'CHANGE_PREREQUISITE_IDS', payload: prerequisiteIds }); - const handleChangeRelease = (release?: Date) => { - setEditableAchievement({ - ...editableAchievement, - release: release - }); - setIsDirty(true); - }; + const handleChangeRelease = (release?: Date) => + dispatch({ type: 'CHANGE_RELEASE', payload: release }); - const handleChangeTitle = (title: string) => { - setEditableAchievement({ - ...editableAchievement, - title: title - }); - setIsDirty(true); - }; + const handleChangeTitle = (title: string) => dispatch({ type: 'CHANGE_TITLE', payload: title }); - const handleChangeView = (view: AchievementView) => { - setEditableAchievement({ - ...editableAchievement, - view: view - }); - setIsDirty(true); - }; + const handleChangeView = (view: AchievementView) => + dispatch({ type: 'CHANGE_VIEW', payload: view }); return (
  • Date: Mon, 17 Aug 2020 15:23:27 +0800 Subject: [PATCH 055/143] Fix bad set state issue --- .../control/achievementEditor/EditableCard.tsx | 4 ++-- .../achievement/control/goalEditor/EditableGoal.tsx | 4 ++-- src/pages/achievement/control/AchievementControl.tsx | 8 -------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index f737e6ff16..d431b6d0b5 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -152,17 +152,17 @@ function EditableCard(props: EditableCardProps) { dispatch({ type: 'DISCARD_CHANGES', payload: achievementClone }); const handleSaveChanges = () => { + dispatch({ type: 'SAVE_CHANGES' }); inferencer.modifyAchievement(editableAchievement); releaseId(id); requestPublish(); - dispatch({ type: 'SAVE_CHANGES' }); }; const handleDeleteAchievement = () => { + dispatch({ type: 'DELETE_ACHIEVEMENT' }); inferencer.removeAchievement(id); releaseId(id); requestPublish(); - dispatch({ type: 'DELETE_ACHIEVEMENT' }); }; const handleChangeAbility = (ability: AchievementAbility) => diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 71f5bb7f75..7664e93bdd 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -74,17 +74,17 @@ function EditableGoal(props: EditableGoalProps) { const handleDiscardChanges = () => dispatch({ type: 'DISCARD_CHANGES', payload: goalClone }); const handleSaveChanges = () => { + dispatch({ type: 'SAVE_CHANGES' }); inferencer.modifyGoalDefinition(editableGoal); releaseId(id); requestPublish(); - dispatch({ type: 'SAVE_CHANGES' }); }; const handleDeleteGoal = () => { + dispatch({ type: 'DELETE_GOAL' }); inferencer.removeGoalDefinition(id); releaseId(id); requestPublish(); - dispatch({ type: 'DELETE_GOAL' }); }; const handleChangeText = (text: string) => dispatch({ type: 'CHANGE_TEXT', payload: text }); diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 4b78598f18..f0192156ab 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -61,14 +61,6 @@ function AchievementControl(props: DispatchProps & StateProps) { /** * Allows editor components to trigger a page re-render so that the AchievementPreview * displays the latest local changes - * - * NOTE: AchievementContext should be able to observe the changes in the inferencer - * and automatically trigger a re-render in all child components. However, in - * modifying an achievement is done by calling - * inferencer.modifyAchievement() instead of using useState hooks recommended by React. - * Hence the AchievementContext is unaware of the changes and a forceRender() is needed. - * - * TODO: Refactor the workflow and deprecate forceRender() */ const [render, setRender] = useState(); const forceRender = () => setRender(!render); From bfea6e07488d72d960300fbde5d7c2d9217ce7e1 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 16:14:21 +0800 Subject: [PATCH 056/143] Change insert before pos to insert at pos --- .../achievementEditor/AchievementSettings.tsx | 2 +- .../utils/AchievementInferencer.ts | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index 5848440736..afe237701f 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -52,7 +52,7 @@ function AchievementSettings(props: AchievementSettingsProps) { placeholder="Enter card background URL here" value={cardBackground} /> -

    Insert before position

    +

    Position

    Note: Select position 0 to hide achievement

    Prerequisites

    diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 82e24230a7..716de2572b 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -139,7 +139,7 @@ class AchievementInferencer { // finally, process the nodeList this.processNodes(); - this.normalizePositions(achievement.id); + this.normalizePositions(); return newId; } @@ -179,7 +179,7 @@ class AchievementInferencer { // then, process the nodeList this.processNodes(); - this.normalizePositions(achievement.id); + this.normalizePositions(achievement.id, achievement.position); } /** @@ -611,24 +611,33 @@ class AchievementInferencer { /** * Reassign the achievement position number without changing their orders. - * If priorityId is supplied, the achievement will be given order priority - * when two achievements have the same position number. + * If anchorId and anchorPosition is supplied, the anchor achievement is + * guaranteed to have its position set at anchorPosition. * - * @param priorityId achievementId + * @param anchorId achievementId */ - private normalizePositions(priorityId?: number) { - const sortedTasks = this.getAllAchievements() - .filter(achievement => achievement.isTask) - .sort((taskA, taskB) => - taskA.position === taskB.position - ? taskA.id === priorityId - ? -1 - : 1 - : taskA.position - taskB.position - ); - + private normalizePositions(anchorId?: number, anchorPosition?: number) { let newPosition = 1; - sortedTasks.forEach(task => (task.position = newPosition++)); + this.getAllAchievements() + .filter(achievement => achievement.isTask) + .sort((taskA, taskB) => taskA.position - taskB.position) + .forEach(sortedTask => (sortedTask.position = newPosition++)); + + // If some achievement got misplaced at the anchorPosition, swap it + // back with the anchor achievement + if (anchorId !== undefined && anchorPosition !== undefined && anchorPosition !== 0) { + const anchorAchievement = this.getAchievement(anchorId); + const newPosition = anchorAchievement.position; + if (newPosition !== anchorPosition) { + const misplacedAchievement = this.getAllAchievements().find( + achievement => achievement.position === anchorPosition + ); + if (misplacedAchievement) { + misplacedAchievement.position = newPosition; + anchorAchievement.position = anchorPosition; + } + } + } } } From 839feac72371d19c24aaac5d04ca23474e6cda64 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 17:18:37 +0800 Subject: [PATCH 057/143] Edit milestone --- .../achievement/overview/AchievementMilestone.tsx | 5 ++++- src/styles/_achievementcontrol.scss | 2 +- src/styles/_achievementdashboard.scss | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/commons/achievement/overview/AchievementMilestone.tsx b/src/commons/achievement/overview/AchievementMilestone.tsx index 636163737d..6bf7696992 100644 --- a/src/commons/achievement/overview/AchievementMilestone.tsx +++ b/src/commons/achievement/overview/AchievementMilestone.tsx @@ -7,7 +7,7 @@ function AchievementMilestone() {
    -

    30

    +

    36

    Complete CS1101S CA Component

    @@ -18,6 +18,9 @@ function AchievementMilestone() {
  • Earn extra 1 MC from CS1010R

    +
    +

    Note: subject to change

    +
    ); } diff --git a/src/styles/_achievementcontrol.scss b/src/styles/_achievementcontrol.scss index 03fbc61378..4ff3b19084 100644 --- a/src/styles/_achievementcontrol.scss +++ b/src/styles/_achievementcontrol.scss @@ -350,7 +350,7 @@ } h3 { - margin: 0.5em 0 0 0; + margin: $default-spacing; } } } diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index cf9f7ed463..f5e67cede6 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -74,7 +74,7 @@ display: flex; flex-direction: column; margin: 20em 0 0 0; - padding: 1em 2.5em; + padding: 0.5em 2.5em; position: absolute; z-index: 2; @@ -95,6 +95,13 @@ padding: 0 0 0 $default-spacing; } } + + .footer { + color: cyan; + font-size: 80%; + text-align: center; + margin: $default-spacing; + } } } From 48c1502c58771593d094f57e0edc50711919b904 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Mon, 17 Aug 2020 17:30:58 +0800 Subject: [PATCH 058/143] Update comments --- src/commons/sagas/RequestsSaga.ts | 2 ++ src/pages/achievement/subcomponents/AchievementDashboard.tsx | 1 - .../achievement/subcomponents/AchievementDashboardContainer.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index a85aaa4a67..d21b8a6e53 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -220,6 +220,7 @@ export async function bulkUpdateAchievements( }); return resp; + // TODO: confirmation notification } /** @@ -239,6 +240,7 @@ export async function bulkUpdateGoals( }); return resp; + // TODO: confirmation notification } /** diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 161542bf3a..da634e5934 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -13,7 +13,6 @@ import { FilterStatus } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { handleGetAchievements: () => void; handleGetOwnGoals: () => void; - // TODO: handleGetGoals: () => void; }; export type StateProps = { diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 29827a1359..2a4a9eca33 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -21,7 +21,6 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis { handleGetAchievements: getAchievements, handleGetOwnGoals: getOwnGoals - // TODO: handleGetGoals: getGoals }, dispatch ); From 0ab3824ee590d6ba9ff1c9f40c6035e2d48a0cec Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 19 Aug 2020 18:36:24 +0800 Subject: [PATCH 059/143] Change forceRender to forceUpdate --- .../control/AchievementControl.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index f0192156ab..d904008f81 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; import { Prompt } from 'react-router'; import AchievementEditor from '../../../commons/achievement/control/AchievementEditor'; @@ -45,8 +45,7 @@ function AchievementControl(props: DispatchProps & StateProps) { /** * Monitors changes that are awaiting publish */ - const publishState = useState(false); - const [awaitPublish, setAwaitPublish] = publishState; + const [awaitPublish, setAwaitPublish] = useState(false); const handlePublish = () => { // NOTE: Update goals first because goals must exist before their ID can be specified in achievements handleBulkUpdateGoals(goals); @@ -55,15 +54,23 @@ function AchievementControl(props: DispatchProps & StateProps) { }; const requestPublish = () => { setAwaitPublish(true); - forceRender(); + forceUpdate(); }; /** - * Allows editor components to trigger a page re-render so that the AchievementPreview - * displays the latest local changes + * Allows editor components to trigger a page re-render whenever the inferencer is modified + * so that the AchievementPreview displays the latest local changes + * + * NOTE: Although the inferencer is passed to the value prop of AchievementContext.Provider, + * changes to the inferencer does not trigger a re-render in all AchievementContext.Consumer + * as expected because Context uses reference identity to determine when to re-render. When + * the editor components update the inferencer by calling inferencer.modifyAchievement(...) + * or inferencer.modifyGoalDefinition(...), the Context does not register the changes hence + * a forceUpdate() hook is needed. + * + * See: https://reactjs.org/docs/context.html#caveats */ - const [render, setRender] = useState(); - const forceRender = () => setRender(!render); + const [, forceUpdate] = useReducer(x => x + 1, 0); return ( From fd64646cb30578dd790a52515610a74bd012ab5d Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 19 Aug 2020 20:17:36 +0800 Subject: [PATCH 060/143] Strongly type editablecard reducer --- .../achievementEditor/AchievementSettings.tsx | 14 +- .../achievementEditor/EditableCard.tsx | 147 ++++++++---------- .../achievementEditor/EditableCardTypes.ts | 73 +++++++++ 3 files changed, 139 insertions(+), 95 deletions(-) create mode 100644 src/commons/achievement/control/achievementEditor/EditableCardTypes.ts diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index afe237701f..9199bdf1cb 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -2,35 +2,29 @@ import { Button, Dialog, EditableText, Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import React, { useContext, useState } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; +import { AchievementItem } from 'src/features/achievement/AchievementTypes'; import EditableGoalIds from './achievementSettings/EditableGoalIds'; import EditablePosition from './achievementSettings/EditablePosition'; import EditablePrerequisiteIds from './achievementSettings/EditablePrerequisiteIds'; type AchievementSettingsProps = { - id: number; - cardBackground: string; changeCardBackground: (cardBackground: string) => void; changeGoalIds: (goalIds: number[]) => void; changePosition: (position: number) => void; changePrerequisiteIds: (prerequisiteIds: number[]) => void; - goalIds: number[]; - position: number; - prerequisiteIds: number[]; + editableAchievement: AchievementItem; }; function AchievementSettings(props: AchievementSettingsProps) { const { - id, - cardBackground, changeCardBackground, changeGoalIds, changePosition, changePrerequisiteIds, - goalIds, - position, - prerequisiteIds + editableAchievement } = props; + const { id, cardBackground, goalIds, position, prerequisiteIds } = editableAchievement; const inferencer = useContext(AchievementContext); diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index d431b6d0b5..774a439ac8 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -12,6 +12,7 @@ import ItemDeleter from '../common/ItemDeleter'; import ItemSaver from '../common/ItemSaver'; import AchievementSettings from './AchievementSettings'; import EditableAbility from './EditableAbility'; +import { Action, ActionType, State } from './EditableCardTypes'; import EditableDate from './EditableDate'; import EditableView from './EditableView'; @@ -21,27 +22,28 @@ type EditableCardProps = { requestPublish: () => void; }; -const reducer = ( - state: { editableAchievement: AchievementItem; isDirty: boolean }, - action: { type: string; payload?: any } -) => { +const init = (achievement: AchievementItem): State => { + return { + editableAchievement: achievement, + isDirty: false + }; +}; + +const reducer = (state: State, action: Action) => { switch (action.type) { - case 'SAVE_CHANGES': + case ActionType.SAVE_CHANGES: return { ...state, isDirty: false }; - case 'DISCARD_CHANGES': - return { - editableAchievement: action.payload, - isDirty: false - }; - case 'DELETE_ACHIEVEMENT': + case ActionType.DISCARD_CHANGES: + return init(action.payload); + case ActionType.DELETE_ACHIEVEMENT: return { ...state, isDirty: false }; - case 'CHANGE_ABILITY': + case ActionType.CHANGE_ABILITY: return { editableAchievement: { ...state.editableAchievement, @@ -49,7 +51,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_CARD_BACKGROUND': + case ActionType.CHANGE_CARD_BACKGROUND: return { editableAchievement: { ...state.editableAchievement, @@ -57,7 +59,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_DEADLINE': + case ActionType.CHANGE_DEADLINE: return { editableAchievement: { ...state.editableAchievement, @@ -65,7 +67,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_GOAL_IDS': + case ActionType.CHANGE_GOAL_IDS: return { editableAchievement: { ...state.editableAchievement, @@ -73,7 +75,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_POSITION': + case ActionType.CHANGE_POSITION: return { editableAchievement: { ...state.editableAchievement, @@ -82,7 +84,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_PREREQUISITE_IDS': + case ActionType.CHANGE_PREREQUISITE_IDS: return { editableAchievement: { ...state.editableAchievement, @@ -90,7 +92,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_RELEASE': + case ActionType.CHANGE_RELEASE: return { editableAchievement: { ...state.editableAchievement, @@ -98,7 +100,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_TITLE': + case ActionType.CHANGE_TITLE: return { editableAchievement: { ...state.editableAchievement, @@ -106,7 +108,7 @@ const reducer = ( }, isDirty: true }; - case 'CHANGE_VIEW': + case ActionType.CHANGE_VIEW: return { editableAchievement: { ...state.editableAchievement, @@ -119,11 +121,6 @@ const reducer = ( } }; -const initialState = { - editableAchievement: {} as AchievementItem, - isDirty: false -}; - function EditableCard(props: EditableCardProps) { const { id, releaseId, requestPublish } = props; @@ -131,65 +128,53 @@ function EditableCard(props: EditableCardProps) { const achievement = inferencer.getAchievement(id); const achievementClone = useMemo(() => cloneDeep(achievement), [achievement]); - const [state, dispatch] = useReducer(reducer, { - ...initialState, - editableAchievement: achievementClone - }); + const [state, dispatch] = useReducer(reducer, achievementClone, init); const { editableAchievement, isDirty } = state; - const { - ability, - cardBackground, - deadline, - goalIds, - position, - prerequisiteIds, - release, - title, - view - } = editableAchievement; - - const handleDiscardChanges = () => - dispatch({ type: 'DISCARD_CHANGES', payload: achievementClone }); - - const handleSaveChanges = () => { - dispatch({ type: 'SAVE_CHANGES' }); + const { ability, cardBackground, deadline, release, title, view } = editableAchievement; + + const saveChanges = () => { + dispatch({ type: ActionType.SAVE_CHANGES }); inferencer.modifyAchievement(editableAchievement); releaseId(id); requestPublish(); }; - const handleDeleteAchievement = () => { - dispatch({ type: 'DELETE_ACHIEVEMENT' }); + const discardChanges = () => + dispatch({ type: ActionType.DISCARD_CHANGES, payload: achievementClone }); + + const deleteAchievement = () => { + dispatch({ type: ActionType.DELETE_ACHIEVEMENT }); inferencer.removeAchievement(id); releaseId(id); requestPublish(); }; - const handleChangeAbility = (ability: AchievementAbility) => - dispatch({ type: 'CHANGE_ABILITY', payload: ability }); + const changeAbility = (ability: AchievementAbility) => + dispatch({ type: ActionType.CHANGE_ABILITY, payload: ability }); - const handleChangeCardBackground = (cardBackground: string) => - dispatch({ type: 'CHANGE_CARD_BACKGROUND', payload: cardBackground }); + const changeCardBackground = (cardBackground: string) => + dispatch({ type: ActionType.CHANGE_CARD_BACKGROUND, payload: cardBackground }); - const handleChangeDeadline = (deadline?: Date) => - dispatch({ type: 'CHANGE_DEADLINE', payload: deadline }); + const changeDeadline = (deadline?: Date) => + dispatch({ type: ActionType.CHANGE_DEADLINE, payload: deadline }); - const handleChangeGoalIds = (goalIds: number[]) => - dispatch({ type: 'CHANGE_GOAL_IDS', payload: goalIds }); + const changeGoalIds = (goalIds: number[]) => + dispatch({ type: ActionType.CHANGE_GOAL_IDS, payload: goalIds }); - const handleChangePosition = (position: number) => - dispatch({ type: 'CHANGE_POSITION', payload: position }); + const changePosition = (position: number) => + dispatch({ type: ActionType.CHANGE_POSITION, payload: position }); - const handleChangePrerequisiteIds = (prerequisiteIds: number[]) => - dispatch({ type: 'CHANGE_PREREQUISITE_IDS', payload: prerequisiteIds }); + const changePrerequisiteIds = (prerequisiteIds: number[]) => + dispatch({ type: ActionType.CHANGE_PREREQUISITE_IDS, payload: prerequisiteIds }); - const handleChangeRelease = (release?: Date) => - dispatch({ type: 'CHANGE_RELEASE', payload: release }); + const changeRelease = (release?: Date) => + dispatch({ type: ActionType.CHANGE_RELEASE, payload: release }); - const handleChangeTitle = (title: string) => dispatch({ type: 'CHANGE_TITLE', payload: title }); + const changeTitle = (title: string) => + dispatch({ type: ActionType.CHANGE_TITLE, payload: title }); - const handleChangeView = (view: AchievementView) => - dispatch({ type: 'CHANGE_VIEW', payload: view }); + const changeView = (view: AchievementView) => + dispatch({ type: ActionType.CHANGE_VIEW, payload: view }); return (
  • {isDirty ? ( - + ) : ( - + )}

    - +

    - - - + + +
    - +
  • diff --git a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts new file mode 100644 index 0000000000..4f5a5749f0 --- /dev/null +++ b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts @@ -0,0 +1,73 @@ +import { + AchievementAbility, + AchievementItem, + AchievementView +} from 'src/features/achievement/AchievementTypes'; + +export enum ActionType { + CHANGE_ABILITY = 'CHANGE_ABILITY', + CHANGE_CARD_BACKGROUND = 'CHANGE_CARD_BACKGROUND', + CHANGE_DEADLINE = 'CHANGE_DEADLINE', + CHANGE_GOAL_IDS = 'CHANGE_GOAL_IDS', + CHANGE_POSITION = 'CHANGE_POSITION', + CHANGE_PREREQUISITE_IDS = 'CHANGE_PREREQUISITE_IDS', + CHANGE_RELEASE = 'CHANGE_RELEASE', + CHANGE_TITLE = 'CHANGE_TITLE', + CHANGE_VIEW = 'CHANGE_VIEW', + DELETE_ACHIEVEMENT = 'DELETE_ACHIEVEMENT', + DISCARD_CHANGES = 'DISCARD_CHANGES', + SAVE_CHANGES = 'SAVE_CHANGES' +} + +export type Action = + | { + type: ActionType.CHANGE_ABILITY; + payload: AchievementAbility; + } + | { + type: ActionType.CHANGE_CARD_BACKGROUND; + payload: string; + } + | { + type: ActionType.CHANGE_DEADLINE; + payload: Date | undefined; + } + | { + type: ActionType.CHANGE_GOAL_IDS; + payload: number[]; + } + | { + type: ActionType.CHANGE_POSITION; + payload: number; + } + | { + type: ActionType.CHANGE_PREREQUISITE_IDS; + payload: number[]; + } + | { + type: ActionType.CHANGE_RELEASE; + payload: Date | undefined; + } + | { + type: ActionType.CHANGE_TITLE; + payload: string; + } + | { + type: ActionType.CHANGE_VIEW; + payload: AchievementView; + } + | { + type: ActionType.DELETE_ACHIEVEMENT; + } + | { + type: ActionType.DISCARD_CHANGES; + payload: AchievementItem; + } + | { + type: ActionType.SAVE_CHANGES; + }; + +export type State = { + editableAchievement: AchievementItem; + isDirty: boolean; +}; From 4b1ec92872e47aa068e0c1ecf5cb7be89896d2fb Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 19 Aug 2020 20:40:11 +0800 Subject: [PATCH 061/143] Strongly type editablegoal --- .../control/goalEditor/EditableGoal.tsx | 60 +++++++++---------- .../control/goalEditor/EditableGoalTypes.ts | 34 +++++++++++ 2 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 src/commons/achievement/control/goalEditor/EditableGoalTypes.ts diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 7664e93bdd..588db94780 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -6,6 +6,7 @@ import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTy import ItemDeleter from '../common/ItemDeleter'; import ItemSaver from '../common/ItemSaver'; +import { Action, ActionType, State } from './EditableGoalTypes'; import EditableMeta from './EditableMeta'; type EditableGoalProps = { @@ -14,39 +15,43 @@ type EditableGoalProps = { requestPublish: () => void; }; -const reducer = ( - state: { editableGoal: GoalDefinition; isDirty: boolean }, - action: { type: string; payload?: any } -) => { +const init = (goal: GoalDefinition): State => { + return { + editableGoal: goal, + isDirty: false + }; +}; + +const reducer = (state: State, action: Action) => { switch (action.type) { - case 'SAVE_CHANGES': + case ActionType.SAVE_CHANGES: return { ...state, isDirty: false }; - case 'DISCARD_CHANGES': + case ActionType.DISCARD_CHANGES: return { editableGoal: action.payload, isDirty: false }; - case 'DELETE_GOAL': + case ActionType.DELETE_GOAL: return { ...state, isDirty: false }; - case 'CHANGE_TEXT': + case ActionType.CHANGE_META: return { editableGoal: { ...state.editableGoal, - text: action.payload + meta: action.payload }, isDirty: true }; - case 'CHANGE_META': + case ActionType.CHANGE_TEXT: return { editableGoal: { ...state.editableGoal, - meta: action.payload + text: action.payload }, isDirty: true }; @@ -55,11 +60,6 @@ const reducer = ( } }; -const initialState = { - editableGoal: {} as GoalDefinition, - isDirty: false -}; - function EditableGoal(props: EditableGoalProps) { const { id, releaseId, requestPublish } = props; @@ -67,44 +67,44 @@ function EditableGoal(props: EditableGoalProps) { const goal = inferencer.getGoalDefinition(id); const goalClone = useMemo(() => cloneDeep(goal), [goal]); - const [state, dispatch] = useReducer(reducer, { ...initialState, editableGoal: goalClone }); + const [state, dispatch] = useReducer(reducer, goalClone, init); const { editableGoal, isDirty } = state; - const { text, meta } = editableGoal; + const { meta, text } = editableGoal; - const handleDiscardChanges = () => dispatch({ type: 'DISCARD_CHANGES', payload: goalClone }); - - const handleSaveChanges = () => { - dispatch({ type: 'SAVE_CHANGES' }); + const saveChanges = () => { + dispatch({ type: ActionType.SAVE_CHANGES }); inferencer.modifyGoalDefinition(editableGoal); releaseId(id); requestPublish(); }; - const handleDeleteGoal = () => { - dispatch({ type: 'DELETE_GOAL' }); + const discardChanges = () => dispatch({ type: ActionType.DISCARD_CHANGES, payload: goalClone }); + + const deleteGoal = () => { + dispatch({ type: ActionType.DELETE_GOAL }); inferencer.removeGoalDefinition(id); releaseId(id); requestPublish(); }; - const handleChangeText = (text: string) => dispatch({ type: 'CHANGE_TEXT', payload: text }); + const changeMeta = (meta: GoalMeta) => dispatch({ type: ActionType.CHANGE_META, payload: meta }); - const handleChangeMeta = (meta: GoalMeta) => dispatch({ type: 'CHANGE_META', payload: meta }); + const changeText = (text: string) => dispatch({ type: ActionType.CHANGE_TEXT, payload: text }); return (
  • {isDirty ? ( - + ) : ( - + )}

    - +

    - +
  • ); diff --git a/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts b/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts new file mode 100644 index 0000000000..c248bcb5d2 --- /dev/null +++ b/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts @@ -0,0 +1,34 @@ +import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTypes'; + +export enum ActionType { + CHANGE_META = 'CHANGE_META', + CHANGE_TEXT = 'CHANGE_TEXT', + DELETE_GOAL = 'DELETE_GOAL', + DISCARD_CHANGES = 'DISCARD_CHANGES', + SAVE_CHANGES = 'SAVE_CHANGES' +} + +export type Action = + | { + type: ActionType.CHANGE_META; + payload: GoalMeta; + } + | { + type: ActionType.CHANGE_TEXT; + payload: string; + } + | { + type: ActionType.DELETE_GOAL; + } + | { + type: ActionType.DISCARD_CHANGES; + payload: GoalDefinition; + } + | { + type: ActionType.SAVE_CHANGES; + }; + +export type State = { + editableGoal: GoalDefinition; + isDirty: boolean; +}; From 284a1a1b67d5e81125c2876febee9decb6f38fc0 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Wed, 19 Aug 2020 20:55:53 +0800 Subject: [PATCH 062/143] Rename handlers --- .../control/AchievementPreview.tsx | 6 ++-- .../achievementEditor/AchievementAdder.tsx | 4 +-- .../achievementEditor/EditableCard.tsx | 2 +- .../achievementSettings/EditableGoalIds.tsx | 8 ++--- .../EditablePrerequisiteIds.tsx | 8 ++--- .../control/common/ItemDeleter.tsx | 10 +++---- .../control/goalEditor/EditableGoal.tsx | 2 +- .../control/goalEditor/EditableMeta.tsx | 5 ++-- .../control/goalEditor/GoalAdder.tsx | 4 +-- .../metaDetails/EditableAssessmentMeta.tsx | 8 ++--- .../metaDetails/EditableBinaryMeta.tsx | 8 ++--- .../metaDetails/EditableManualMeta.tsx | 4 +-- .../control/AchievementControl.tsx | 30 +++++++++---------- .../control/AchievementControlContainer.ts | 8 ++--- .../subcomponents/AchievementDashboard.tsx | 12 ++++---- .../AchievementDashboardContainer.ts | 4 +-- 16 files changed, 62 insertions(+), 61 deletions(-) diff --git a/src/commons/achievement/control/AchievementPreview.tsx b/src/commons/achievement/control/AchievementPreview.tsx index 2c6b53536d..b548eca33e 100644 --- a/src/commons/achievement/control/AchievementPreview.tsx +++ b/src/commons/achievement/control/AchievementPreview.tsx @@ -9,11 +9,11 @@ import AchievementView from '../AchievementView'; type AchievementPreviewProps = { awaitPublish: boolean; - handlePublish: () => void; + publishChanges: () => void; }; function AchievementPreview(props: AchievementPreviewProps) { - const { awaitPublish, handlePublish } = props; + const { awaitPublish, publishChanges } = props; const inferencer = useContext(AchievementContext); @@ -44,7 +44,7 @@ function AchievementPreview(props: AchievementPreviewProps) { icon={IconNames.CLOUD_UPLOAD} intent="primary" text="Publish Changes" - onClick={handlePublish} + onClick={publishChanges} /> )}
    diff --git a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx index 128799f53f..1391af900f 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementAdder.tsx @@ -15,14 +15,14 @@ function AchievementAdder(props: AchievementAdderProps) { const inferencer = useContext(AchievementContext); - const handleAddAchievement = () => setNewId(inferencer.insertAchievement(achievementTemplate)); + const addAchievement = () => setNewId(inferencer.insertAchievement(achievementTemplate)); return (
    diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx index 1fef54344f..778d9af306 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx @@ -25,13 +25,13 @@ function EditableGoalIds(props: EditableGoalIdsProps) { const selectedGoals = new Set(goalIds); const availableGoals = new Set(without(allGoalIds, ...goalIds)); - const handleSelectGoal = (selectId: number) => { + const selectGoal = (selectId: number) => { selectedGoals.add(selectId); availableGoals.delete(selectId); changeGoalIds([...selectedGoals]); }; - const handleRemoveGoal = (removeId?: number) => { + const removeGoal = (removeId?: number) => { if (removeId === undefined) return; selectedGoals.delete(removeId); @@ -44,9 +44,9 @@ function EditableGoalIds(props: EditableGoalIdsProps) { itemRenderer={goalRenderer} items={[...availableGoals]} noResults={} - onItemSelect={handleSelectGoal} + onItemSelect={selectGoal} selectedItems={[...selectedGoals]} - tagInputProps={{ onRemove: text => handleRemoveGoal(getId(text)) }} + tagInputProps={{ onRemove: text => removeGoal(getId(text)) }} tagRenderer={getText} /> ); diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx index ee2a31819f..cf9ae1cd0e 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx @@ -24,13 +24,13 @@ function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { const selectedPrereqs = new Set(prerequisiteIds); const availablePrereqs = new Set(availableIds); - const handleSelectPrereq = (selectId: number) => { + const selectPrereq = (selectId: number) => { selectedPrereqs.add(selectId); availablePrereqs.delete(selectId); changePrerequisiteIds([...selectedPrereqs]); }; - const handleRemovePrereq = (removeId?: number) => { + const removePrereq = (removeId?: number) => { if (removeId === undefined) return; selectedPrereqs.delete(removeId); @@ -43,9 +43,9 @@ function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { itemRenderer={prerequisiteRenderer} items={[...availablePrereqs]} noResults={} - onItemSelect={handleSelectPrereq} + onItemSelect={selectPrereq} selectedItems={[...selectedPrereqs]} - tagInputProps={{ onRemove: title => handleRemovePrereq(getId(title)) }} + tagInputProps={{ onRemove: title => removePrereq(getId(title)) }} tagRenderer={getTitle} /> ); diff --git a/src/commons/achievement/control/common/ItemDeleter.tsx b/src/commons/achievement/control/common/ItemDeleter.tsx index 9fe675a575..2f6d60c186 100644 --- a/src/commons/achievement/control/common/ItemDeleter.tsx +++ b/src/commons/achievement/control/common/ItemDeleter.tsx @@ -4,14 +4,14 @@ import React from 'react'; import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; type ItemDeleterProps = { - handleDelete: () => void; + deleteItem: () => void; item: string; }; function ItemDeleter(props: ItemDeleterProps) { - const { handleDelete, item } = props; + const { deleteItem, item } = props; - const handleConfirmDelete = async () => { + const confirmDelete = async () => { const confirm = await showSimpleConfirmDialog({ contents: `Are you sure you want to delete '${item}' ?`, negativeLabel: 'No', @@ -19,13 +19,13 @@ function ItemDeleter(props: ItemDeleterProps) { positiveLabel: 'Yes, delete' }); if (confirm) { - handleDelete(); + deleteItem(); } }; return ( -

    diff --git a/src/commons/achievement/control/goalEditor/EditableMeta.tsx b/src/commons/achievement/control/goalEditor/EditableMeta.tsx index b2c445c0ce..0d1a13da7e 100644 --- a/src/commons/achievement/control/goalEditor/EditableMeta.tsx +++ b/src/commons/achievement/control/goalEditor/EditableMeta.tsx @@ -22,7 +22,8 @@ function EditableMeta(props: EditableMetaProps) { ); - const handleChangeType = (type: GoalType) => changeMeta(metaTemplate(type)); + // TODO: memo? + const changeType = (type: GoalType) => changeMeta(metaTemplate(type)); const editableMetaDetails = (type: GoalType) => { switch (type) { @@ -44,7 +45,7 @@ function EditableMeta(props: EditableMetaProps) { filterable={false} itemRenderer={typeRenderer} items={Object.values(GoalType)} - onItemSelect={handleChangeType} + onItemSelect={changeType} >

    diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 3b44012148..900010c10e 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -12,7 +12,11 @@ import ItemDeleter from '../common/ItemDeleter'; import ItemSaver from '../common/ItemSaver'; import AchievementSettings from './AchievementSettings'; import EditableAbility from './EditableAbility'; -import { Action, ActionType, State } from './EditableCardTypes'; +import { + EditableCardAction as Action, + EditableCardActionType as ActionType, + EditableCardState as State +} from './EditableCardTypes'; import EditableDate from './EditableDate'; import EditableView from './EditableView'; diff --git a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts index 4f5a5749f0..4373f7e51b 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts +++ b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts @@ -4,7 +4,7 @@ import { AchievementView } from 'src/features/achievement/AchievementTypes'; -export enum ActionType { +export enum EditableCardActionType { CHANGE_ABILITY = 'CHANGE_ABILITY', CHANGE_CARD_BACKGROUND = 'CHANGE_CARD_BACKGROUND', CHANGE_DEADLINE = 'CHANGE_DEADLINE', @@ -19,55 +19,55 @@ export enum ActionType { SAVE_CHANGES = 'SAVE_CHANGES' } -export type Action = +export type EditableCardAction = | { - type: ActionType.CHANGE_ABILITY; + type: EditableCardActionType.CHANGE_ABILITY; payload: AchievementAbility; } | { - type: ActionType.CHANGE_CARD_BACKGROUND; + type: EditableCardActionType.CHANGE_CARD_BACKGROUND; payload: string; } | { - type: ActionType.CHANGE_DEADLINE; + type: EditableCardActionType.CHANGE_DEADLINE; payload: Date | undefined; } | { - type: ActionType.CHANGE_GOAL_IDS; + type: EditableCardActionType.CHANGE_GOAL_IDS; payload: number[]; } | { - type: ActionType.CHANGE_POSITION; + type: EditableCardActionType.CHANGE_POSITION; payload: number; } | { - type: ActionType.CHANGE_PREREQUISITE_IDS; + type: EditableCardActionType.CHANGE_PREREQUISITE_IDS; payload: number[]; } | { - type: ActionType.CHANGE_RELEASE; + type: EditableCardActionType.CHANGE_RELEASE; payload: Date | undefined; } | { - type: ActionType.CHANGE_TITLE; + type: EditableCardActionType.CHANGE_TITLE; payload: string; } | { - type: ActionType.CHANGE_VIEW; + type: EditableCardActionType.CHANGE_VIEW; payload: AchievementView; } | { - type: ActionType.DELETE_ACHIEVEMENT; + type: EditableCardActionType.DELETE_ACHIEVEMENT; } | { - type: ActionType.DISCARD_CHANGES; + type: EditableCardActionType.DISCARD_CHANGES; payload: AchievementItem; } | { - type: ActionType.SAVE_CHANGES; + type: EditableCardActionType.SAVE_CHANGES; }; -export type State = { +export type EditableCardState = { editableAchievement: AchievementItem; isDirty: boolean; }; diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx index 778d9af306..2953cf0a60 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalIds.tsx @@ -5,15 +5,15 @@ import React, { useContext } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; type EditableGoalIdsProps = { - allGoalIds: number[]; changeGoalIds: (goalIds: number[]) => void; goalIds: number[]; }; function EditableGoalIds(props: EditableGoalIdsProps) { - const { allGoalIds, changeGoalIds, goalIds } = props; + const { changeGoalIds, goalIds } = props; const inferencer = useContext(AchievementContext); + const allGoalIds = inferencer.getAllGoalIds(); const getId = (text: string) => inferencer.getIdByText(text); const getText = (id: number) => inferencer.getTextById(id); diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx index cf9ae1cd0e..5435c84fd1 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteIds.tsx @@ -4,15 +4,16 @@ import React, { useContext } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; type EditablePrerequisiteIdsProps = { - availableIds: number[]; changePrerequisiteIds: (prerequisiteIds: number[]) => void; + id: number; prerequisiteIds: number[]; }; function EditablePrerequisiteIds(props: EditablePrerequisiteIdsProps) { - const { availableIds, changePrerequisiteIds, prerequisiteIds } = props; + const { changePrerequisiteIds, id, prerequisiteIds } = props; const inferencer = useContext(AchievementContext); + const availableIds = inferencer.listAvailablePrerequisiteIds(id); const getId = (title: string) => inferencer.getIdByTitle(title); const getTitle = (id: number) => inferencer.getTitleById(id); diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 4bfbbe060e..8c59405df4 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -6,7 +6,11 @@ import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTy import ItemDeleter from '../common/ItemDeleter'; import ItemSaver from '../common/ItemSaver'; -import { Action, ActionType, State } from './EditableGoalTypes'; +import { + EditableGoalAction as Action, + EditableGoalActionType as ActionType, + EditableGoalState as State +} from './EditableGoalTypes'; import EditableMeta from './EditableMeta'; type EditableGoalProps = { @@ -30,10 +34,7 @@ const reducer = (state: State, action: Action) => { isDirty: false }; case ActionType.DISCARD_CHANGES: - return { - editableGoal: action.payload, - isDirty: false - }; + return init(action.payload); case ActionType.DELETE_GOAL: return { ...state, diff --git a/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts b/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts index c248bcb5d2..6799598a56 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts +++ b/src/commons/achievement/control/goalEditor/EditableGoalTypes.ts @@ -1,6 +1,6 @@ import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTypes'; -export enum ActionType { +export enum EditableGoalActionType { CHANGE_META = 'CHANGE_META', CHANGE_TEXT = 'CHANGE_TEXT', DELETE_GOAL = 'DELETE_GOAL', @@ -8,27 +8,27 @@ export enum ActionType { SAVE_CHANGES = 'SAVE_CHANGES' } -export type Action = +export type EditableGoalAction = | { - type: ActionType.CHANGE_META; + type: EditableGoalActionType.CHANGE_META; payload: GoalMeta; } | { - type: ActionType.CHANGE_TEXT; + type: EditableGoalActionType.CHANGE_TEXT; payload: string; } | { - type: ActionType.DELETE_GOAL; + type: EditableGoalActionType.DELETE_GOAL; } | { - type: ActionType.DISCARD_CHANGES; + type: EditableGoalActionType.DISCARD_CHANGES; payload: GoalDefinition; } | { - type: ActionType.SAVE_CHANGES; + type: EditableGoalActionType.SAVE_CHANGES; }; -export type State = { +export type EditableGoalState = { editableGoal: GoalDefinition; isDirty: boolean; }; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 716de2572b..a9fda323f4 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -614,7 +614,8 @@ class AchievementInferencer { * If anchorId and anchorPosition is supplied, the anchor achievement is * guaranteed to have its position set at anchorPosition. * - * @param anchorId achievementId + * @param anchorId anchor achievementId + * @param anchorPosition anchor position */ private normalizePositions(anchorId?: number, anchorPosition?: number) { let newPosition = 1; @@ -625,7 +626,7 @@ class AchievementInferencer { // If some achievement got misplaced at the anchorPosition, swap it // back with the anchor achievement - if (anchorId !== undefined && anchorPosition !== undefined && anchorPosition !== 0) { + if (anchorId && anchorPosition && anchorPosition !== 0) { const anchorAchievement = this.getAchievement(anchorId); const newPosition = anchorAchievement.position; if (newPosition !== anchorPosition) { From 7b434c2b1e46be95c2e03a943f3821d7d2e2ed7d Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Thu, 20 Aug 2020 21:14:33 +0800 Subject: [PATCH 068/143] Update mocks, sort control item order --- .../achievement/control/AchievementEditor.tsx | 2 +- .../achievement/control/GoalEditor.tsx | 2 +- src/commons/mocks/AchievementMocks.ts | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index cdf87f34a0..afb7a02dcc 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -44,7 +44,7 @@ function AchievementEditor(props: AchievementEditorProps) {
      - {generateEditableCards(inferencer.getAllAchievementIds().reverse())} + {generateEditableCards(inferencer.getAllAchievementIds().sort((a, b) => b - a))}
    ); diff --git a/src/commons/achievement/control/GoalEditor.tsx b/src/commons/achievement/control/GoalEditor.tsx index 4f17d23068..0e1bb1495a 100644 --- a/src/commons/achievement/control/GoalEditor.tsx +++ b/src/commons/achievement/control/GoalEditor.tsx @@ -44,7 +44,7 @@ function GoalEditor(props: GoalEditorProps) {
      - {generateEditableGoals(inferencer.getAllGoalIds().reverse())} + {generateEditableGoals(inferencer.getAllGoalIds().sort((a, b) => b - a))}
    ); diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index e164c8f0ac..62b1fa6c9c 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -136,14 +136,14 @@ export const mockAchievements: AchievementItem[] = [ } }, { - id: 7, + id: 21, title: 'The Source-rer', ability: AchievementAbility.EFFORT, deadline: new Date(2020, 7, 21, 0, 0, 0), isTask: true, position: 3, prerequisiteIds: [], - goalIds: [6, 7], + goalIds: [16, 18], cardBackground: 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-background/the-source-rer.png', view: { @@ -191,13 +191,13 @@ export const mockAchievements: AchievementItem[] = [ } }, { - id: 10, + id: 16, title: "That's the Spirit", ability: AchievementAbility.EXPLORATION, isTask: true, position: 5, prerequisiteIds: [], - goalIds: [9], + goalIds: [14], cardBackground: 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-background/thats-the-spirit.png', view: { @@ -209,13 +209,13 @@ export const mockAchievements: AchievementItem[] = [ } }, { - id: 11, + id: 13, title: 'Kool Kidz', ability: AchievementAbility.FLEX, isTask: true, position: 6, prerequisiteIds: [], - goalIds: [10], + goalIds: [11], cardBackground: 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-background/default.png', view: { @@ -313,7 +313,7 @@ export const mockGoals: AchievementGoal[] = [ completed: false }, { - id: 6, + id: 16, text: 'Submit Source 3 Path', meta: { type: GoalType.BINARY, @@ -328,7 +328,7 @@ export const mockGoals: AchievementGoal[] = [ completed: true }, { - id: 7, + id: 18, text: 'XP earned from Source 3 Path', meta: { type: GoalType.ASSESSMENT, @@ -351,7 +351,7 @@ export const mockGoals: AchievementGoal[] = [ completed: false }, { - id: 9, + id: 14, text: 'Submit 1 PR to Source Academy Github', meta: { type: GoalType.MANUAL, @@ -362,7 +362,7 @@ export const mockGoals: AchievementGoal[] = [ completed: true }, { - id: 10, + id: 11, text: 'Be the Koolest Kidz in SOC by redeeming this 100 XP achievement yourself', meta: { type: GoalType.BINARY, From f960d17a566b8bfa961e24d57508a94104065802 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Thu, 20 Aug 2020 21:15:37 +0800 Subject: [PATCH 069/143] Update inferencer tests --- .../__tests__/AchievementInferencer.test.ts | 164 ++++++++++++++++++ .../utils/__tests__/Inferencer.test.ts | 37 ---- 2 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts delete mode 100644 src/commons/achievement/utils/__tests__/Inferencer.test.ts diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts new file mode 100644 index 0000000000..28271d61d1 --- /dev/null +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -0,0 +1,164 @@ +import { mockAchievements, mockGoals } from 'src/commons/mocks/AchievementMocks'; +import { + AchievementAbility, + AchievementGoal, + AchievementItem, + GoalType +} from 'src/features/achievement/AchievementTypes'; + +import AchievementInferencer from '../AchievementInferencer'; + +const testAchievement: AchievementItem = { + id: 0, + title: 'Test Achievement', + ability: AchievementAbility.CORE, + isTask: false, + prerequisiteIds: [], + goalIds: [], + position: 0, + cardBackground: + 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-background/default.png', + view: { + coverImage: + 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/cover-image/default.png', + description: 'This is a test achievement', + completionText: `Congratulations! You've completed the test achievement!` + } +}; + +const testGoal: AchievementGoal = { + id: 0, + text: 'Test Goal', + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: 'M1A', + requiredCompletionFrac: 1 + }, + xp: 0, + maxXp: 0, + completed: false +}; + +describe('Achievement Inferencer Constructor', () => { + test('Accepts empty achievements and goals', () => { + const inferencer = new AchievementInferencer([], []); + + expect(inferencer.getAllAchievements()).toEqual([]); + expect(inferencer.getAllGoals()).toEqual([]); + }); + + test('Accepts non-empty achievements and empty goals', () => { + const inferencer = new AchievementInferencer([testAchievement], []); + + expect(inferencer.getAllAchievements()).toEqual([testAchievement]); + expect(inferencer.getAllGoals()).toEqual([]); + }); + + test('Accepts empty achievements and non-empty goals', () => { + const inferencer = new AchievementInferencer([], [testGoal]); + + expect(inferencer.getAllAchievements()).toEqual([]); + expect(inferencer.getAllGoals()).toEqual([testGoal]); + }); + + test('Accepts non-empty achievements and non-empty goals', () => { + const inferencer = new AchievementInferencer(mockAchievements, mockGoals); + + expect(inferencer.getAllAchievements()).toEqual(mockAchievements); + expect(inferencer.getAllGoals()).toEqual(mockGoals); + }); + + describe('Expected Overlapping ID Behaviors', () => { + const testAchievement1: AchievementItem = { ...testAchievement, id: 1 }; + const testAchievement2: AchievementItem = { ...testAchievement, id: 2 }; + const testAchievement3: AchievementItem = { ...testAchievement, id: 2 }; + const testGoal1: AchievementGoal = { ...testGoal, id: 1 }; + const testGoal2: AchievementGoal = { ...testGoal, id: 1 }; + const testGoal3: AchievementGoal = { ...testGoal, id: 2 }; + + const inferencer = new AchievementInferencer( + [testAchievement1, testAchievement2, testAchievement3], + [testGoal1, testGoal2, testGoal3] + ); + + test('Overwrites items of same IDs', () => { + expect(inferencer.getAllAchievements()).toEqual([testAchievement1, testAchievement2]); + expect(inferencer.getAllGoals()).toEqual([testGoal1, testGoal3]); + }); + + test('References the correct achievements and goals', () => { + expect(inferencer.getAchievement(1)).toBe(testAchievement1); + expect(inferencer.getAchievement(2)).not.toBe(testAchievement2); + expect(inferencer.getAchievement(2)).toBe(testAchievement3); + expect(inferencer.getGoal(1)).not.toBe(testGoal1); + expect(inferencer.getGoal(1)).toBe(testGoal2); + expect(inferencer.getGoal(2)).toBe(testGoal3); + expect(inferencer.getGoalDefinition(1)).not.toBe(testGoal1); + expect(inferencer.getGoalDefinition(1)).toBe(testGoal2); + expect(inferencer.getGoalDefinition(2)).toBe(testGoal3); + }); + }); +}); + +describe('Achievement Inferencer Getter', () => { + const inferencer = new AchievementInferencer(mockAchievements, mockGoals); + + test('Get all achievement IDs', () => { + const achievementIds = [0, 1, 2, 3, 4, 5, 6, 8, 9, 13, 16, 21]; + + expect(inferencer.getAllAchievementIds().sort((a, b) => a - b)).toEqual(achievementIds); + }); + + test('Get all goal IDs', () => { + const goalIds = [0, 1, 2, 3, 4, 5, 8, 11, 14, 16, 18]; + + expect(inferencer.getAllGoalIds().sort((a, b) => a - b)).toEqual(goalIds); + }); + + test('List task IDs', () => { + const taskIds = [0, 4, 8, 13, 16, 21]; + + expect(inferencer.listTaskIds().sort((a, b) => a - b)).toEqual(taskIds); + }); + + test('List sorted task IDs', () => { + const sortedTaskIds = [0, 8, 21, 4, 16, 13]; + + expect(inferencer.listSortedTaskIds()).toEqual(sortedTaskIds); + }); + + test('List goals', () => { + const testAchievement1 = { ...testAchievement, id: 123, goalIds: [456, 123] }; + const testAchievement2 = { ...testAchievement, id: 456, goalIds: [] }; + const testGoal1 = { ...testGoal, id: 123 }; + const testGoal2 = { ...testGoal, id: 456 }; + + const inferencer = new AchievementInferencer( + [testAchievement1, testAchievement2], + [testGoal1, testGoal2] + ); + + expect(inferencer.listGoals(123)[0]).toBe(testGoal2); + expect(inferencer.listGoals(123)[1]).toBe(testGoal1); + expect(inferencer.listGoals(456)).toEqual([]); + }); + + test('List prerequisite goals', () => { + const testAchievement1 = { + ...testAchievement, + id: 123, + prerequisiteIds: [456] + }; + const testAchievement2 = { ...testAchievement, id: 456, goalIds: [456, 123] }; + const testGoal1 = { ...testGoal, id: 123 }; + const testGoal2 = { ...testGoal, id: 456 }; + + const inferencer = new AchievementInferencer( + [testAchievement1, testAchievement2], + [testGoal1, testGoal2] + ); + + expect(inferencer.listPrerequisiteGoals(123)[0]).toBe(testGoal2); + expect(inferencer.listPrerequisiteGoals(123)[1]).toBe(testGoal1); + }); +}); diff --git a/src/commons/achievement/utils/__tests__/Inferencer.test.ts b/src/commons/achievement/utils/__tests__/Inferencer.test.ts deleted file mode 100644 index 3e6a5ccf52..0000000000 --- a/src/commons/achievement/utils/__tests__/Inferencer.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - AchievementAbility, - AchievementItem -} from '../../../../features/achievement/AchievementTypes'; -import { mockAchievements, mockGoals } from '../../../mocks/AchievementMocks'; -import AchievementInferencer from '../AchievementInferencer'; - -const sampleAchievement: AchievementItem = { - id: 12, - title: 'New Achievement', - ability: AchievementAbility.CORE, - isTask: false, - prerequisiteIds: [], - goalIds: [], - position: 0, - cardBackground: - 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-background/default.png', - view: { - coverImage: - 'https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/cover-image/default.png', - description: '', - completionText: '' - } -}; - -describe('Achievements change when', () => { - test('an achievement is inserted and deleted', () => { - const inferencer = new AchievementInferencer(mockAchievements, mockGoals); - - inferencer.insertAchievement(sampleAchievement); - expect(inferencer.getAllAchievements().length).toEqual(13); - expect(inferencer.getAchievement(sampleAchievement.id)).toEqual(sampleAchievement); - - inferencer.removeAchievement(sampleAchievement.id); - expect(inferencer.getAllAchievements().length).toEqual(12); - }); -}); From 28e7f28f003934ed5bab174277677e56e2018169 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 22 Aug 2020 21:40:44 +0800 Subject: [PATCH 070/143] Add inferencer test --- .../__tests__/AchievementInferencer.test.ts | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 28271d61d1..2fbb3056e7 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -30,9 +30,8 @@ const testGoal: AchievementGoal = { id: 0, text: 'Test Goal', meta: { - type: GoalType.ASSESSMENT, - assessmentNumber: 'M1A', - requiredCompletionFrac: 1 + type: GoalType.MANUAL, + maxXp: 0 }, xp: 0, maxXp: 0, @@ -162,3 +161,53 @@ describe('Achievement Inferencer Getter', () => { expect(inferencer.listPrerequisiteGoals(123)[1]).toBe(testGoal1); }); }); + +describe('Achievement ID to Title', () => { + const achievementId = 123; + const achievementTitle = 'AcH1Ev3m3Nt t1tL3 h3R3'; + const testAchievement1: AchievementItem = { + ...testAchievement, + id: achievementId, + title: achievementTitle + }; + const inferencer = new AchievementInferencer([testAchievement1], []); + + test('Returns undefined for non-existing ID', () => { + expect(inferencer.getTitleById(1)).toBeUndefined(); + }); + + test('Returns achievement title for existing ID', () => { + expect(inferencer.getTitleById(achievementId)).toBe(achievementTitle); + }); + + test('Returns undefined for non-existing achievement title', () => { + expect(inferencer.getIdByTitle('IUisL0v3')).toBeUndefined(); + }); + + test('Returns ID for existing achievement title', () => { + expect(inferencer.getIdByTitle(achievementTitle)).toBe(achievementId); + }); +}); + +describe('Goal ID to Text', () => { + const goalId = 123; + const goalText = 'g0@L T3xt h3R3'; + const testGoal1: AchievementGoal = { ...testGoal, id: goalId, text: goalText }; + const inferencer = new AchievementInferencer([], [testGoal1]); + + test('Returns undefined for non-existing ID', () => { + expect(inferencer.getTextById(1)).toBeUndefined(); + }); + + test('Returns goal text for existing ID', () => { + expect(inferencer.getTextById(goalId)).toBe(goalText); + }); + + test('Returns undefined for non-existing goal text', () => { + expect(inferencer.getIdByText('IUisL0v3')).toBeUndefined(); + }); + + test('Returns ID for existing goal text', () => { + expect(inferencer.getIdByText(goalText)).toBe(goalId); + }); +}); From 7409a3e8320a2002b75ad30d79856c9e6dd303ef Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 22 Aug 2020 21:42:11 +0800 Subject: [PATCH 071/143] Add xp attribute to inferencer node --- .../achievement/utils/AchievementInferencer.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index a9fda323f4..a7d694f90f 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -15,6 +15,7 @@ import { isExpired } from './DateHelper'; * * @param {AchievementItem} achievement the achievement item * @param {Date | undefined} displayDeadline deadline displayed on the achievement card + * @param {number} xp attained XP of the achievement * @param {number} maxXp maximum attainable XP of the achievement * @param {number} progressFrac progress percentage in fraction. It is always between 0 to 1, both inclusive. * @param {AchievementStatus} status the achievement status @@ -24,6 +25,7 @@ import { isExpired } from './DateHelper'; class AchievementNode { public achievement: AchievementItem; public displayDeadline?: Date; + public xp: number; public maxXp: number; public progressFrac: number; public status: AchievementStatus; @@ -35,6 +37,7 @@ class AchievementNode { this.achievement = achievement; this.displayDeadline = deadline; + this.xp = 0; this.maxXp = 0; this.progressFrac = 0; this.status = AchievementStatus.ACTIVE; @@ -337,8 +340,8 @@ class AchievementInferencer { * @param id Achievement Id */ public getAchievementXp(id: number) { - const { goalIds } = this.getAchievement(id); - return goalIds.reduce((xp, goalId) => xp + this.getGoal(goalId).xp, 0); + assert(this.nodeList.has(id), `achievement ${id} not found`); + return this.nodeList.get(id)!.xp; } /** @@ -450,7 +453,7 @@ class AchievementInferencer { this.nodeList.forEach(node => { this.generateDescendant(node); this.generateDisplayDeadline(node); - this.generateMaxXp(node); + this.generateXpAndMaxXp(node); this.generateProgressFrac(node); this.generateStatus(node); @@ -553,12 +556,13 @@ class AchievementInferencer { } /** - * Calculates the achievement maximum attainable XP + * Calculates the achievement attained XP and maximum attainable XP * * @param node the AchievementNode */ - private generateMaxXp(node: AchievementNode) { + private generateXpAndMaxXp(node: AchievementNode) { const { goalIds } = node.achievement; + node.xp = goalIds.reduce((xp, goalId) => xp + this.getGoal(goalId).xp, 0); node.maxXp = goalIds.reduce((maxXp, goalId) => maxXp + this.getGoal(goalId).maxXp, 0); } From 7085156f7c9ca445171bff996f01a8e4bf69bff5 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 22 Aug 2020 22:18:57 +0800 Subject: [PATCH 072/143] Remove duplicate prereqId and goalId --- .../achievement/utils/AchievementInferencer.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index a7d694f90f..c71b713a9b 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import { uniq } from 'lodash'; import { showDangerMessage } from '../../../commons/utils/NotificationsHelper'; import { @@ -356,6 +357,9 @@ class AchievementInferencer { /** * Returns total XP earned from all goals + * + * Note: Goals that do not belong to any achievement is also added into the total XP + * calculation */ public getTotalXp() { return this.getAllGoals().reduce((totalXp, goal) => totalXp + goal.xp, 0); @@ -451,14 +455,17 @@ class AchievementInferencer { this.titleToId = new Map(); this.nodeList.forEach(node => { + const { title, id } = node.achievement; + this.titleToId.set(title, id); + + node.achievement.prerequisiteIds = uniq(node.achievement.prerequisiteIds); + node.achievement.goalIds = uniq(node.achievement.goalIds); + this.generateDescendant(node); this.generateDisplayDeadline(node); this.generateXpAndMaxXp(node); this.generateProgressFrac(node); this.generateStatus(node); - - const { title, id } = node.achievement; - this.titleToId.set(title, id); }); } @@ -504,7 +511,7 @@ class AchievementInferencer { for (const childId of node.descendant) { if (childId === node.achievement.id) { const { title } = node.achievement; - // NOTE: not the best error handling practice, but as long as admin verifies the + // Note: not the best error handling practice, but as long as admin verifies the // data in AchievementPreview and do not publish new achievements with circular // dependency error, it should be suffice showDangerMessage(`Circular dependency detected in achievement ${title}`, 30000); From e90c0b2927fe080563aedb4db295823b3c8994b5 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sat, 22 Aug 2020 22:35:45 +0800 Subject: [PATCH 073/143] Add xp system tests --- .../__tests__/AchievementInferencer.test.ts | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 2fbb3056e7..1c14ef32c5 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -71,6 +71,7 @@ describe('Achievement Inferencer Constructor', () => { const testAchievement1: AchievementItem = { ...testAchievement, id: 1 }; const testAchievement2: AchievementItem = { ...testAchievement, id: 2 }; const testAchievement3: AchievementItem = { ...testAchievement, id: 2 }; + const testGoal1: AchievementGoal = { ...testGoal, id: 1 }; const testGoal2: AchievementGoal = { ...testGoal, id: 1 }; const testGoal3: AchievementGoal = { ...testGoal, id: 2 }; @@ -127,38 +128,42 @@ describe('Achievement Inferencer Getter', () => { }); test('List goals', () => { - const testAchievement1 = { ...testAchievement, id: 123, goalIds: [456, 123] }; - const testAchievement2 = { ...testAchievement, id: 456, goalIds: [] }; - const testGoal1 = { ...testGoal, id: 123 }; - const testGoal2 = { ...testGoal, id: 456 }; + const testAchievement1: AchievementItem = { ...testAchievement, id: 1, goalIds: [2, 1] }; + const testAchievement2: AchievementItem = { ...testAchievement, id: 2, goalIds: [] }; + + const testGoal1: AchievementGoal = { ...testGoal, id: 1 }; + const testGoal2: AchievementGoal = { ...testGoal, id: 2 }; const inferencer = new AchievementInferencer( [testAchievement1, testAchievement2], [testGoal1, testGoal2] ); - expect(inferencer.listGoals(123)[0]).toBe(testGoal2); - expect(inferencer.listGoals(123)[1]).toBe(testGoal1); - expect(inferencer.listGoals(456)).toEqual([]); + expect(inferencer.listGoals(1).length).toBe(2); + expect(inferencer.listGoals(1)[0]).toBe(testGoal2); + expect(inferencer.listGoals(1)[1]).toBe(testGoal1); + expect(inferencer.listGoals(2)).toEqual([]); }); test('List prerequisite goals', () => { - const testAchievement1 = { + const testAchievement1: AchievementItem = { ...testAchievement, - id: 123, - prerequisiteIds: [456] + id: 1, + prerequisiteIds: [2] }; - const testAchievement2 = { ...testAchievement, id: 456, goalIds: [456, 123] }; - const testGoal1 = { ...testGoal, id: 123 }; - const testGoal2 = { ...testGoal, id: 456 }; + const testAchievement2: AchievementItem = { ...testAchievement, id: 2, goalIds: [2, 1] }; + + const testGoal1: AchievementGoal = { ...testGoal, id: 1 }; + const testGoal2: AchievementGoal = { ...testGoal, id: 2 }; const inferencer = new AchievementInferencer( [testAchievement1, testAchievement2], [testGoal1, testGoal2] ); - expect(inferencer.listPrerequisiteGoals(123)[0]).toBe(testGoal2); - expect(inferencer.listPrerequisiteGoals(123)[1]).toBe(testGoal1); + expect(inferencer.listPrerequisiteGoals(1).length).toBe(2); + expect(inferencer.listPrerequisiteGoals(1)[0]).toBe(testGoal2); + expect(inferencer.listPrerequisiteGoals(1)[1]).toBe(testGoal1); }); }); @@ -170,6 +175,7 @@ describe('Achievement ID to Title', () => { id: achievementId, title: achievementTitle }; + const inferencer = new AchievementInferencer([testAchievement1], []); test('Returns undefined for non-existing ID', () => { @@ -193,6 +199,7 @@ describe('Goal ID to Text', () => { const goalId = 123; const goalText = 'g0@L T3xt h3R3'; const testGoal1: AchievementGoal = { ...testGoal, id: goalId, text: goalText }; + const inferencer = new AchievementInferencer([], [testGoal1]); test('Returns undefined for non-existing ID', () => { @@ -211,3 +218,32 @@ describe('Goal ID to Text', () => { expect(inferencer.getIdByText(goalText)).toBe(goalId); }); }); + +describe('Achievement XP System', () => { + const testAchievement1: AchievementItem = { ...testAchievement, id: 1, goalIds: [1, 2] }; + + const testGoal1: AchievementGoal = { ...testGoal, id: 1, xp: 100, maxXp: 100 }; + const testGoal2: AchievementGoal = { ...testGoal, id: 2, xp: 20, maxXp: 100 }; + const testGoal3: AchievementGoal = { ...testGoal, id: 3, xp: 3, maxXp: 100 }; + + const inferencer = new AchievementInferencer( + [testAchievement1], + [testGoal1, testGoal2, testGoal3] + ); + + test('Returns XP earned from an achievement', () => { + expect(inferencer.getAchievementXp(1)).toBe(120); + }); + + test('Returns Max XP earned from an achievement', () => { + expect(inferencer.getAchievementMaxXp(1)).toBe(200); + }); + + test('Returns Total XP earned from all goals', () => { + expect(inferencer.getTotalXp()).toBe(123); + }); + + test('Returns progress frac from an achievement', () => { + expect(inferencer.getProgressFrac(1)).toBeCloseTo(120 / 200); + }); +}); From 85e797b30fa45fed848b0fa6e41c5c555498c2c3 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 23 Aug 2020 04:14:37 +0800 Subject: [PATCH 074/143] Add prereq tests --- .../__tests__/AchievementInferencer.test.ts | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 1c14ef32c5..23cd94e21a 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -219,24 +219,85 @@ describe('Goal ID to Text', () => { }); }); +describe('Achievement Prerequisite System', () => { + const testAchievement1: AchievementItem = { + ...testAchievement, + id: 1, + prerequisiteIds: [2, 3] + }; + const testAchievement2: AchievementItem = { ...testAchievement, id: 2 }; + const testAchievement3: AchievementItem = { ...testAchievement, id: 3, prerequisiteIds: [4] }; + const testAchievement4: AchievementItem = { ...testAchievement, id: 4, prerequisiteIds: [5] }; + const testAchievement5: AchievementItem = { ...testAchievement, id: 5 }; + + const inferencer = new AchievementInferencer( + [testAchievement1, testAchievement2, testAchievement3, testAchievement4, testAchievement5], + [] + ); + + test('Is immediate children', () => { + expect(inferencer.isImmediateChild(1, 2)).toBeTruthy(); + expect(inferencer.isImmediateChild(1, 3)).toBeTruthy(); + expect(inferencer.isImmediateChild(1, 4)).toBeFalsy(); + expect(inferencer.isImmediateChild(1, 5)).toBeFalsy(); + expect(inferencer.isImmediateChild(1, 101)).toBeFalsy(); + }); + + test('Get immediate children', () => { + expect(inferencer.getImmediateChildren(1)).toEqual(new Set([2, 3])); + expect(inferencer.getImmediateChildren(2)).toEqual(new Set()); + expect(inferencer.getImmediateChildren(3)).toEqual(new Set([4])); + expect(inferencer.getImmediateChildren(4)).toEqual(new Set([5])); + expect(inferencer.getImmediateChildren(101)).toEqual(new Set()); + }); + + test('Is descendant', () => { + expect(inferencer.isDescendant(1, 2)).toBeTruthy(); + expect(inferencer.isDescendant(1, 3)).toBeTruthy(); + expect(inferencer.isDescendant(1, 4)).toBeTruthy(); + expect(inferencer.isDescendant(1, 5)).toBeTruthy(); + expect(inferencer.isDescendant(1, 101)).toBeFalsy(); + }); + + test('Get descendants', () => { + expect(inferencer.getDescendants(1)).toEqual(new Set([2, 3, 4, 5])); + expect(inferencer.getDescendants(2)).toEqual(new Set()); + expect(inferencer.getDescendants(3)).toEqual(new Set([4, 5])); + expect(inferencer.getDescendants(4)).toEqual(new Set([5])); + expect(inferencer.getDescendants(101)).toEqual(new Set()); + }); + + test('List available prerequisite IDs', () => { + expect(inferencer.listAvailablePrerequisiteIds(1)).toEqual([]); + expect(inferencer.listAvailablePrerequisiteIds(2)).toEqual([3, 4, 5]); + expect(inferencer.listAvailablePrerequisiteIds(3)).toEqual([2]); + expect(inferencer.listAvailablePrerequisiteIds(4)).toEqual([2]); + expect(inferencer.listAvailablePrerequisiteIds(5)).toEqual([2]); + expect(inferencer.listAvailablePrerequisiteIds(101)).toEqual([1, 2, 3, 4, 5]); + }); +}); + describe('Achievement XP System', () => { const testAchievement1: AchievementItem = { ...testAchievement, id: 1, goalIds: [1, 2] }; + const testAchievement2: AchievementItem = { ...testAchievement, id: 2, goalIds: [] }; const testGoal1: AchievementGoal = { ...testGoal, id: 1, xp: 100, maxXp: 100 }; const testGoal2: AchievementGoal = { ...testGoal, id: 2, xp: 20, maxXp: 100 }; const testGoal3: AchievementGoal = { ...testGoal, id: 3, xp: 3, maxXp: 100 }; const inferencer = new AchievementInferencer( - [testAchievement1], + [testAchievement1, testAchievement2], [testGoal1, testGoal2, testGoal3] ); test('Returns XP earned from an achievement', () => { expect(inferencer.getAchievementXp(1)).toBe(120); + expect(inferencer.getAchievementXp(2)).toBe(0); }); test('Returns Max XP earned from an achievement', () => { expect(inferencer.getAchievementMaxXp(1)).toBe(200); + expect(inferencer.getAchievementMaxXp(2)).toBe(0); }); test('Returns Total XP earned from all goals', () => { @@ -245,5 +306,8 @@ describe('Achievement XP System', () => { test('Returns progress frac from an achievement', () => { expect(inferencer.getProgressFrac(1)).toBeCloseTo(120 / 200); + expect(inferencer.getProgressFrac(2)).toBe(0); }); }); + +describe('Achievement Deadline & Status System', () => {}); From 9c74aeee25e26440a3687c03e02868e26eff1924 Mon Sep 17 00:00:00 2001 From: Jet Kan Date: Sun, 23 Aug 2020 04:30:12 +0800 Subject: [PATCH 075/143] Remove asserts --- ...entTemplate.tsx => AchievementTemplate.ts} | 2 +- .../control/goalEditor/GoalAdder.tsx | 4 +- .../{GoalTemplate.tsx => GoalTemplate.ts} | 24 +++++++++-- .../utils/AchievementInferencer.ts | 41 ++++++++----------- .../achievement/AchievementConstants.ts | 5 +-- 5 files changed, 41 insertions(+), 35 deletions(-) rename src/commons/achievement/control/achievementEditor/{AchievementTemplate.tsx => AchievementTemplate.ts} (98%) rename src/commons/achievement/control/goalEditor/{GoalTemplate.tsx => GoalTemplate.ts} (56%) diff --git a/src/commons/achievement/control/achievementEditor/AchievementTemplate.tsx b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts similarity index 98% rename from src/commons/achievement/control/achievementEditor/AchievementTemplate.tsx rename to src/commons/achievement/control/achievementEditor/AchievementTemplate.ts index b673261e9c..d8fcf78856 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementTemplate.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts @@ -15,7 +15,7 @@ export const viewTemplate: AchievementView = { }; export const achievementTemplate: AchievementItem = { - id: 0, + id: -1, title: 'Achievement Title Here', ability: AchievementAbility.CORE, isTask: false, diff --git a/src/commons/achievement/control/goalEditor/GoalAdder.tsx b/src/commons/achievement/control/goalEditor/GoalAdder.tsx index ad71ff5286..32df558c18 100644 --- a/src/commons/achievement/control/goalEditor/GoalAdder.tsx +++ b/src/commons/achievement/control/goalEditor/GoalAdder.tsx @@ -3,7 +3,7 @@ import { IconNames } from '@blueprintjs/icons'; import React, { useContext } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; -import { goalTemplate } from './GoalTemplate'; +import { goalDefinitionTemplate } from './GoalTemplate'; type GoalAdderProps = { allowNewId: boolean; @@ -15,7 +15,7 @@ function GoalAdder(props: GoalAdderProps) { const inferencer = useContext(AchievementContext); - const addGoal = () => setNewId(inferencer.insertGoalDefinition(goalTemplate)); + const addGoal = () => setNewId(inferencer.insertGoalDefinition(goalDefinitionTemplate)); return (
diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 6859164e48..8ab3f465e5 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -159,6 +159,14 @@ class AchievementInferencer { return this.goalsToDelete; } + /** + * Resets the array so the same achievement/goal is not deleted twice + */ + public resetToDelete() { + this.achievementsToDelete = []; + this.goalsToDelete = [] + } + /** * Inserts a new AchievementItem into the Inferencer, * then returns the newly assigned achievementUuid diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 7f77d43960..c6b7c2b36b 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -14,7 +14,6 @@ import { import { backendifyGoalDefinition, backendifyGoalProgress, - convertBackendMetaToFrontend } from '../../features/achievement/AchievementBackender'; import { AchievementAbility, @@ -185,7 +184,7 @@ export const getGoals = async ( ({ uuid: goal.uuid || '', text: goal.text || '', - meta: convertBackendMetaToFrontend(goal.meta) as GoalMeta, + meta: goal.meta as GoalMeta, xp: goal.xp, maxXp: goal.maxXp, completed: goal.completed @@ -213,7 +212,7 @@ export const getOwnGoals = async (tokens: Tokens): Promise ({ ...goal, uuid: goal.uuid }); - -export const convertBackendMetaToFrontend = (meta: any) => { - if (meta.type === 'Binary' || meta.type === 'Manual') { - meta.maxXp = meta.max_xp; - delete meta.max_xp; - } else if (meta.type === 'Assessment') { - meta.assessmentNumber = meta.assessment_number; - delete meta.assessment_number; - meta.requiredCompletionFrac = meta.required_completion_frac; - delete meta.required_completion_frac; - } - return meta; -}; diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 865c818604..a0ac0486d7 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -53,6 +53,7 @@ function AchievementControl(props: DispatchProps & StateProps) { bulkUpdateAchievements(inferencer.getAllAchievements()); inferencer.getAchievementsToDelete().forEach(removeAchievement); inferencer.getGoalsToDelete().forEach(removeGoal); + inferencer.resetToDelete(); setAwaitPublish(false); }; const requestPublish = () => { From 2daa3c187e06463a513be1bc332929d9ffa7f1bd Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 22 Feb 2021 09:39:24 +0800 Subject: [PATCH 101/143] Moved AchievementBackender to commons Also removed backendifyGoalProgress because it was doing nothing --- .../achievement/utils}/AchievementBackender.ts | 9 ++------- src/commons/sagas/RequestsSaga.ts | 7 ++----- 2 files changed, 4 insertions(+), 12 deletions(-) rename src/{features/achievement => commons/achievement/utils}/AchievementBackender.ts (55%) diff --git a/src/features/achievement/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts similarity index 55% rename from src/features/achievement/AchievementBackender.ts rename to src/commons/achievement/utils/AchievementBackender.ts index 0699bec060..69a8dae78a 100644 --- a/src/features/achievement/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -1,4 +1,4 @@ -import { GoalDefinition, GoalProgress, GoalType } from './AchievementTypes'; +import { GoalDefinition, GoalType } from '../../../features/achievement/AchievementTypes'; export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ maxXp: goal.meta.type === GoalType.ASSESSMENT ? 0 : goal.meta.maxXp, @@ -6,9 +6,4 @@ export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ text: goal.text, type: goal.meta.type, uuid: goal.uuid -}); - -export const backendifyGoalProgress = (goal: GoalProgress) => ({ - ...goal, - uuid: goal.uuid -}); +}); \ No newline at end of file diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index c6b7c2b36b..e7daec21e4 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -11,10 +11,6 @@ import { QuestionType, QuestionTypes } from '../../commons/assessment/AssessmentTypes'; -import { - backendifyGoalDefinition, - backendifyGoalProgress, -} from '../../features/achievement/AchievementBackender'; import { AchievementAbility, AchievementGoal, @@ -31,6 +27,7 @@ import { } from '../../features/remoteExecution/RemoteExecutionTypes'; import { PlaybackData, SourcecastData } from '../../features/sourceRecorder/SourceRecorderTypes'; import { store } from '../../pages/createStore'; +import { backendifyGoalDefinition } from '../achievement/utils/AchievementBackender'; import { Tokens, User } from '../application/types/SessionTypes'; import { Notification } from '../notificationBadge/NotificationBadgeTypes'; import { actions } from '../utils/ActionsHelper'; @@ -309,7 +306,7 @@ export const updateGoalProgress = async ( ): Promise => { const resp = await request(`achievements/goals/${progress.uuid}/${studentId}`, 'POST', { ...tokens, - body: { progress: backendifyGoalProgress(progress) }, + body: { progress: progress }, noHeaderAccept: true, shouldAutoLogout: false, shouldRefresh: true From 4b6e084d6844e377c603939283bd7e459e5119df Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 22 Feb 2021 16:20:41 +0800 Subject: [PATCH 102/143] Fix fomatting issues --- src/commons/achievement/utils/AchievementBackender.ts | 2 +- src/commons/achievement/utils/AchievementInferencer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 69a8dae78a..3084c870af 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -6,4 +6,4 @@ export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ text: goal.text, type: goal.meta.type, uuid: goal.uuid -}); \ No newline at end of file +}); diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 8ab3f465e5..738a9a08f3 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -164,7 +164,7 @@ class AchievementInferencer { */ public resetToDelete() { this.achievementsToDelete = []; - this.goalsToDelete = [] + this.goalsToDelete = []; } /** From 7d811704ab723f7a1071f2395617d28f61e13204 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Wed, 3 Mar 2021 23:31:29 +0800 Subject: [PATCH 103/143] Allows for staff to edit manual goal progress --- .../achievement/AchievementManualEditor.tsx | 97 +++++++++++++++++++ src/commons/achievement/utils/eventHandler.ts | 15 +++ src/commons/sagas/RequestsSaga.ts | 2 + src/commons/sideContent/SideContent.tsx | 2 + .../subcomponents/AchievementDashboard.tsx | 9 +- .../AchievementDashboardContainer.ts | 8 +- src/styles/_achievementdashboard.scss | 27 ++++++ 7 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/commons/achievement/AchievementManualEditor.tsx create mode 100644 src/commons/achievement/utils/eventHandler.ts diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx new file mode 100644 index 0000000000..fdbbd92140 --- /dev/null +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -0,0 +1,97 @@ +import { Button, MenuItem, NumericInput } from '@blueprintjs/core'; +import { ItemRenderer, Select } from '@blueprintjs/select'; +import { useContext, useState } from 'react'; +import { AchievementContext } from 'src/features/achievement/AchievementConstants'; +import { AchievementGoal, GoalProgress } from 'src/features/achievement/AchievementTypes'; + +type AchievementManualEditorProps = { + studio: string; + updateGoalProgress: (studentId: number, progress: GoalProgress) => void; +}; + +const GoalSelect = Select.ofType(); +const goalRenderer: ItemRenderer = (goal, { handleClick }) => ( + +); + +function AchievementManualEditor(props: AchievementManualEditorProps) { + const { studio, updateGoalProgress } = props; + + const inferencer = useContext(AchievementContext); + const manualAchievements: AchievementGoal[] = inferencer.getAllGoals().filter( + goals => goals.meta.type === 'Manual' + ); + + const [goal, changeGoal] = useState(manualAchievements[0]); + const [userId, changeUserId] = useState(0); + const [xp, changeXp] = useState(goal ? goal.xp : 0); + + const updateGoal = () => { + if (goal) { + const progress: GoalProgress = { + uuid: goal.uuid, + xp: xp, + maxXp: goal.maxXp, + completed: xp >= goal.maxXp + } + updateGoalProgress(userId, progress); + } + }; + + if (studio !== 'Staff') { + // For the studio's avenger to manually assign to his students + // In theory, just copy paste the bottom code, but make userID a select + return ( +
+

{studio}

+
+ ) + } else { + return ( +
+

User ID:

+ + +

Goal:

+ +
+ ) + } +} + +export default AchievementManualEditor; \ No newline at end of file diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts new file mode 100644 index 0000000000..1a04f6ef3e --- /dev/null +++ b/src/commons/achievement/utils/eventHandler.ts @@ -0,0 +1,15 @@ +import { getOwnGoals, updateGoalProgress } from '../../../features/achievement/AchievementActions'; +import { store } from '../../../pages/createStore'; + +export function incrementFirstMaxXp() { + store.dispatch(getOwnGoals()); + const goals = store.getState().achievement.goals; + const progress = { + uuid: goals[0].uuid, + xp: goals[0].xp + 1, + maxXp: goals[0].maxXp, + completed: goals[0].completed + } + const userId = store.getState().session.userId; + userId && store.dispatch(updateGoalProgress(userId, progress)); +} \ No newline at end of file diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index e7daec21e4..ab21d2c470 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -204,6 +204,8 @@ export const getOwnGoals = async (tokens: Tokens): Promise ({ diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx index c46e094938..01f0f0a7c9 100644 --- a/src/commons/sideContent/SideContent.tsx +++ b/src/commons/sideContent/SideContent.tsx @@ -2,6 +2,7 @@ import { Card, Icon, Tab, TabId, Tabs, Tooltip } from '@blueprintjs/core'; import * as React from 'react'; import { useSelector } from 'react-redux'; +import { incrementFirstMaxXp } from '../achievement/utils/eventHandler'; import { OverallState } from '../application/ApplicationTypes'; import { DebuggerContext, WorkspaceLocation } from '../workspace/WorkspaceTypes'; import { getDynamicTabs } from './SideContentHelper'; @@ -128,6 +129,7 @@ const SideContent = (props: SideContentProps) => { * To be run when tabs are changed. * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs. */ + incrementFirstMaxXp(); const resetAlert = (prevTabId: TabId) => { const iconId = generateIconId(prevTabId); const icon = document.getElementById(iconId); diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index d0ca0d95d8..6ecec6cc50 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -1,24 +1,28 @@ import { IconNames } from '@blueprintjs/icons'; import React, { useEffect, useState } from 'react'; +import { Role } from 'src/commons/application/ApplicationTypes'; import AchievementFilter from '../../../commons/achievement/AchievementFilter'; +import AchievementManualEditor from '../../../commons/achievement/AchievementManualEditor'; import AchievementOverview from '../../../commons/achievement/AchievementOverview'; import AchievementTask from '../../../commons/achievement/AchievementTask'; import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import Constants from '../../../commons/utils/Constants'; import { AchievementContext } from '../../../features/achievement/AchievementConstants'; -import { FilterStatus } from '../../../features/achievement/AchievementTypes'; +import { FilterStatus, GoalProgress } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { getAchievements: () => void; getOwnGoals: () => void; + updateGoalProgress: (studentId: number, progress: GoalProgress) => void }; export type StateProps = { group: string | null; inferencer: AchievementInferencer; name?: string; + role?: Role }; /** @@ -43,7 +47,7 @@ export const generateAchievementTasks = ( )); function Dashboard(props: DispatchProps & StateProps) { - const { group, getAchievements, getOwnGoals, inferencer, name } = props; + const { group, getAchievements, getOwnGoals, updateGoalProgress, inferencer, name, role } = props; /** * Fetch the latest achievements and goals from backend when the page is rendered @@ -69,6 +73,7 @@ function Dashboard(props: DispatchProps & StateProps) {
+ {role != Role.Student && }
diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 21e001eb66..c2d2bc2f91 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -5,7 +5,7 @@ import AchievementInferencer from '../../../commons/achievement/utils/Achievemen import { OverallState } from '../../../commons/application/ApplicationTypes'; import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import Constants from '../../../commons/utils/Constants'; -import { getAchievements, getOwnGoals } from '../../../features/achievement/AchievementActions'; +import { getAchievements, getOwnGoals, updateGoalProgress } from '../../../features/achievement/AchievementActions'; import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; const mapStateToProps: MapStateToProps = state => ({ @@ -13,14 +13,16 @@ const mapStateToProps: MapStateToProps = state => inferencer: Constants.useAchievementBackend ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) : new AchievementInferencer(mockAchievements, mockGoals), - name: state.session.name + name: state.session.name, + role: state.session.role }); const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { getAchievements, - getOwnGoals + getOwnGoals, + updateGoalProgress }, dispatch ); diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index f5e67cede6..546d30ea6b 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -111,6 +111,33 @@ } } + .achievement-manual-editor { + $default-spacing: 0.5em; + + align-items: center; + display: flex; + flex-direction: row; + padding: $default-spacing; + color: white; + + input { + display: inline-flex; + margin: 0 $default-spacing; + } + + button { + background: white; + display: inline-flex; + margin: 0 $default-spacing; + + } + + h3 { + margin: 0 $default-spacing; + display: inline-flex; + } + } + .achievement-main { $border-glow-radius: 10px; // Cover aspect ratio 2:1 From 853c363fe4a241dff3d0607ebf79cddabe851756 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Tue, 9 Mar 2021 20:48:59 +0800 Subject: [PATCH 104/143] Fixed style issues --- .../achievement/AchievementManualEditor.tsx | 47 ++++++++----------- src/commons/achievement/utils/eventHandler.ts | 10 ++-- src/commons/sideContent/SideContent.tsx | 1 + .../subcomponents/AchievementDashboard.tsx | 11 +++-- .../AchievementDashboardContainer.ts | 6 ++- src/styles/_achievementdashboard.scss | 3 +- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index fdbbd92140..a996241d7d 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -18,14 +18,14 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { const { studio, updateGoalProgress } = props; const inferencer = useContext(AchievementContext); - const manualAchievements: AchievementGoal[] = inferencer.getAllGoals().filter( - goals => goals.meta.type === 'Manual' - ); + const manualAchievements: AchievementGoal[] = inferencer + .getAllGoals() + .filter(goals => goals.meta.type === 'Manual'); const [goal, changeGoal] = useState(manualAchievements[0]); const [userId, changeUserId] = useState(0); const [xp, changeXp] = useState(goal ? goal.xp : 0); - + const updateGoal = () => { if (goal) { const progress: GoalProgress = { @@ -33,9 +33,9 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { xp: xp, maxXp: goal.maxXp, completed: xp >= goal.maxXp - } + }; updateGoalProgress(userId, progress); - } + } }; if (studio !== 'Staff') { @@ -45,7 +45,7 @@ function AchievementManualEditor(props: AchievementManualEditorProps) {

{studio}

- ) + ); } else { return (
@@ -65,33 +65,24 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { itemRenderer={goalRenderer} onItemSelect={changeGoal} > -
- ) + ); } } -export default AchievementManualEditor; \ No newline at end of file +export default AchievementManualEditor; diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 1a04f6ef3e..b3c28358e9 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -5,11 +5,11 @@ export function incrementFirstMaxXp() { store.dispatch(getOwnGoals()); const goals = store.getState().achievement.goals; const progress = { - uuid: goals[0].uuid, - xp: goals[0].xp + 1, - maxXp: goals[0].maxXp, + uuid: goals[0].uuid, + xp: goals[0].xp + 1, + maxXp: goals[0].maxXp, completed: goals[0].completed - } + }; const userId = store.getState().session.userId; userId && store.dispatch(updateGoalProgress(userId, progress)); -} \ No newline at end of file +} diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx index 01f0f0a7c9..773b2b9cea 100644 --- a/src/commons/sideContent/SideContent.tsx +++ b/src/commons/sideContent/SideContent.tsx @@ -129,6 +129,7 @@ const SideContent = (props: SideContentProps) => { * To be run when tabs are changed. * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs. */ + // Achievements test code! incrementFirstMaxXp(); const resetAlert = (prevTabId: TabId) => { const iconId = generateIconId(prevTabId); diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 6ecec6cc50..963d1a65a7 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -15,14 +15,14 @@ import { FilterStatus, GoalProgress } from '../../../features/achievement/Achiev export type DispatchProps = { getAchievements: () => void; getOwnGoals: () => void; - updateGoalProgress: (studentId: number, progress: GoalProgress) => void + updateGoalProgress: (studentId: number, progress: GoalProgress) => void; }; export type StateProps = { group: string | null; inferencer: AchievementInferencer; name?: string; - role?: Role + role?: Role; }; /** @@ -73,7 +73,12 @@ function Dashboard(props: DispatchProps & StateProps) {
- {role != Role.Student && } + {role != Role.Student && ( + + )}
diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index c2d2bc2f91..467841e3d2 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -5,7 +5,11 @@ import AchievementInferencer from '../../../commons/achievement/utils/Achievemen import { OverallState } from '../../../commons/application/ApplicationTypes'; import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import Constants from '../../../commons/utils/Constants'; -import { getAchievements, getOwnGoals, updateGoalProgress } from '../../../features/achievement/AchievementActions'; +import { + getAchievements, + getOwnGoals, + updateGoalProgress +} from '../../../features/achievement/AchievementActions'; import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; const mapStateToProps: MapStateToProps = state => ({ diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index 546d30ea6b..b25d44c497 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -119,7 +119,7 @@ flex-direction: row; padding: $default-spacing; color: white; - + input { display: inline-flex; margin: 0 $default-spacing; @@ -129,7 +129,6 @@ background: white; display: inline-flex; margin: 0 $default-spacing; - } h3 { From eff188a5bcef76412ac9df674353bdbacb660f62 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Sat, 13 Mar 2021 21:55:22 +0800 Subject: [PATCH 105/143] Added EventHandler to be called when event happens --- .../achievement/AchievementManualEditor.tsx | 1 + .../control/goalEditor/EditableMeta.tsx | 4 + .../control/goalEditor/GoalTemplate.ts | 8 ++ .../metaDetails/EditableEventMeta.tsx | 62 ++++++++++++++ src/commons/achievement/utils/eventHandler.ts | 82 ++++++++++++++++--- src/commons/sagas/RequestsSaga.ts | 2 - src/commons/sideContent/SideContent.tsx | 5 +- src/features/achievement/AchievementTypes.ts | 17 +++- 8 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index a996241d7d..985e463b0c 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -39,6 +39,7 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { }; if (studio !== 'Staff') { + // TODO // For the studio's avenger to manually assign to his students // In theory, just copy paste the bottom code, but make userID a select return ( diff --git a/src/commons/achievement/control/goalEditor/EditableMeta.tsx b/src/commons/achievement/control/goalEditor/EditableMeta.tsx index e7ba10047d..30522f6946 100644 --- a/src/commons/achievement/control/goalEditor/EditableMeta.tsx +++ b/src/commons/achievement/control/goalEditor/EditableMeta.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { AssessmentMeta, BinaryMeta, + EventMeta, GoalMeta, GoalType, ManualMeta @@ -12,6 +13,7 @@ import { import { metaTemplate } from './GoalTemplate'; import EditableAssessmentMeta from './metaDetails/EditableAssessmentMeta'; import EditableBinaryMeta from './metaDetails/EditableBinaryMeta'; +import EditableEventMeta from './metaDetails/EditableEventMeta'; import EditableManualMeta from './metaDetails/EditableManualMeta'; type EditableMetaProps = { @@ -40,6 +42,8 @@ function EditableMeta(props: EditableMetaProps) { return ; case GoalType.MANUAL: return ; + case GoalType.EVENT: + return ; default: return null; } diff --git a/src/commons/achievement/control/goalEditor/GoalTemplate.ts b/src/commons/achievement/control/goalEditor/GoalTemplate.ts index 129d6f1b7b..a7dc59ef86 100644 --- a/src/commons/achievement/control/goalEditor/GoalTemplate.ts +++ b/src/commons/achievement/control/goalEditor/GoalTemplate.ts @@ -1,5 +1,6 @@ import { AchievementGoal, + EventType, GoalDefinition, GoalMeta, GoalProgress, @@ -25,6 +26,13 @@ export const metaTemplate = (type: GoalType): GoalMeta => { type: GoalType.MANUAL, maxXp: 0 }; + case GoalType.EVENT: + return { + type: GoalType.EVENT, + eventName: EventType.NONE, + targetCount: 0, + maxXp: 0 + }; } }; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx new file mode 100644 index 0000000000..48a12c4bff --- /dev/null +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx @@ -0,0 +1,62 @@ +import { Button, MenuItem, NumericInput, Tooltip } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { ItemRenderer, Select } from '@blueprintjs/select'; +import { EventMeta, EventType, GoalMeta } from 'src/features/achievement/AchievementTypes'; + +type EditableEventMetaProps = { + changeMeta: (meta: GoalMeta) => void; + eventMeta: EventMeta; +}; + +const EventSelect = Select.ofType(); +const eventRenderer: ItemRenderer = (goal, { handleClick }) => ( + +); + +function EditableEventMeta(props: EditableEventMetaProps) { + const { changeMeta, eventMeta } = props; + const { eventName, targetCount, maxXp } = eventMeta; + + const changeMaxXp = (maxXp: number) => changeMeta({ ...eventMeta, maxXp: maxXp }); + + const changeTargetCount = (targetCount: number) => + changeMeta({ ...eventMeta, targetCount: targetCount }); + + const changeEventName = (eventName: EventType) => + changeMeta({ ...eventMeta, eventName: eventName }); + + return ( + <> + +
); } diff --git a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts index f30c4d2332..1ce6d37f9b 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts +++ b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts @@ -18,6 +18,7 @@ export const achievementTemplate: AchievementItem = { uuid: '', title: 'Achievement Title Here', ability: AchievementAbility.CORE, + xp: 0, isTask: false, position: 0, prerequisiteUuids: [], diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index a09fa59c99..41ca3770bb 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -1,4 +1,4 @@ -import { EditableText } from '@blueprintjs/core'; +import { EditableText, NumericInput } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; import React, { useContext, useMemo, useReducer } from 'react'; @@ -120,6 +120,14 @@ const reducer = (state: State, action: Action) => { }, isDirty: true }; + case ActionType.CHANGE_XP: + return { + editableAchievement: { + ...state.editableAchievement, + xp: action.payload + }, + isDirty: true + }; default: return state; } @@ -134,7 +142,7 @@ function EditableCard(props: EditableCardProps) { const [state, dispatch] = useReducer(reducer, achievementClone, init); const { editableAchievement, isDirty } = state; - const { ability, cardBackground, deadline, release, title, view } = editableAchievement; + const { ability, cardBackground, deadline, release, title, view, xp } = editableAchievement; const saveChanges = () => { dispatch({ type: ActionType.SAVE_CHANGES }); @@ -162,8 +170,22 @@ function EditableCard(props: EditableCardProps) { const changeDeadline = (deadline?: Date) => dispatch({ type: ActionType.CHANGE_DEADLINE, payload: deadline }); - const changeGoalUuids = (goalUuids: string[]) => + const changeGoalUuids = (goalUuids: string[]) => { dispatch({ type: ActionType.CHANGE_GOAL_UUIDS, payload: goalUuids }); + // add the current achievement into the goals chosen + goalUuids.forEach(goalUuid => { + const goal = inferencer.getGoal(goalUuid); + // iterate through the achievements, if the uuid is ot found, add it in + const len = goal.achievementUuids.length; + for (let i = 0; i < len; i++) { + if (goal.achievementUuids[i] === uuid) { + return; + } + } + goal.achievementUuids[len] = uuid; + } + ); + }; const changePosition = (position: number) => dispatch({ type: ActionType.CHANGE_POSITION, payload: position }); @@ -180,6 +202,8 @@ function EditableCard(props: EditableCardProps) { const changeView = (view: AchievementView) => dispatch({ type: ActionType.CHANGE_VIEW, payload: view }); + const changeXp = (xp: number) => dispatch({ type: ActionType.CHANGE_XP, payload: xp }); + return (
  • +
    + +
    diff --git a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts index 41e298ea31..a04ba73cc9 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts +++ b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts @@ -14,6 +14,7 @@ export enum EditableCardActionType { CHANGE_RELEASE = 'CHANGE_RELEASE', CHANGE_TITLE = 'CHANGE_TITLE', CHANGE_VIEW = 'CHANGE_VIEW', + CHANGE_XP = 'CHANGE_XP', DELETE_ACHIEVEMENT = 'DELETE_ACHIEVEMENT', DISCARD_CHANGES = 'DISCARD_CHANGES', SAVE_CHANGES = 'SAVE_CHANGES' @@ -56,6 +57,10 @@ export type EditableCardAction = type: EditableCardActionType.CHANGE_VIEW; payload: AchievementView; } + | { + type: EditableCardActionType.CHANGE_XP; + payload: number; + } | { type: EditableCardActionType.DELETE_ACHIEVEMENT; } diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index 14aca066ed..bcfa578c29 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -93,7 +93,7 @@ function EditableGoal(props: EditableGoalProps) { const changeText = (text: string) => dispatch({ type: ActionType.CHANGE_TEXT, payload: text }); return ( -
  • +
  • {isDirty ? ( diff --git a/src/commons/achievement/control/goalEditor/GoalTemplate.ts b/src/commons/achievement/control/goalEditor/GoalTemplate.ts index 7625ca886d..1e48700f2a 100644 --- a/src/commons/achievement/control/goalEditor/GoalTemplate.ts +++ b/src/commons/achievement/control/goalEditor/GoalTemplate.ts @@ -13,27 +13,26 @@ export const metaTemplate = (type: GoalType): GoalMeta => { case GoalType.ASSESSMENT: return { type: GoalType.ASSESSMENT, - assessmentNumber: '', + assessmentNumber: 0, requiredCompletionFrac: 0 }; case GoalType.BINARY: return { type: GoalType.BINARY, condition: false, - maxXp: 0 + targetCount: 1 }; case GoalType.MANUAL: return { type: GoalType.MANUAL, - maxXp: 0 + targetCount: 1 }; case GoalType.EVENT: return { type: GoalType.EVENT, eventNames: [EventType.NONE], - targetCount: 0, - condition: { type: EventConditions.NONE, leftBound: NaN, rightBound: NaN }, - maxXp: 0 + targetCount: 1, + condition: { type: EventConditions.NONE, leftBound: NaN, rightBound: NaN } }; } }; @@ -41,13 +40,14 @@ export const metaTemplate = (type: GoalType): GoalMeta => { export const goalDefinitionTemplate: GoalDefinition = { uuid: '', text: 'Goal Text Here', + achievementUuids: [], meta: metaTemplate(GoalType.MANUAL) }; const goalProgressTemplate: GoalProgress = { uuid: '', - xp: 0, - maxXp: 0, + count: 0, + targetCount: 1, completed: false }; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableAssessmentMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableAssessmentMeta.tsx index f7c0a05a87..84b5d7eebf 100644 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableAssessmentMeta.tsx +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableAssessmentMeta.tsx @@ -1,4 +1,4 @@ -import { EditableText, NumericInput, Tooltip } from '@blueprintjs/core'; +import { NumericInput, Tooltip } from '@blueprintjs/core'; import { AssessmentMeta, GoalMeta } from 'src/features/achievement/AchievementTypes'; type EditableAssessmentMetaProps = { @@ -10,7 +10,7 @@ function EditableAssessmentMeta(props: EditableAssessmentMetaProps) { const { assessmentMeta, changeMeta } = props; const { assessmentNumber, requiredCompletionFrac } = assessmentMeta; - const changeAssessmentNumber = (assessmentNumber: string) => + const changeAssessmentNumber = (assessmentNumber: number) => changeMeta({ ...assessmentMeta, assessmentNumber: assessmentNumber }); const changeRequiredCompletion = (requiredCompletion: number) => { @@ -21,8 +21,9 @@ function EditableAssessmentMeta(props: EditableAssessmentMetaProps) { return ( <> - diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableBinaryMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableBinaryMeta.tsx index 64791c1c71..a7824fed51 100644 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableBinaryMeta.tsx +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableBinaryMeta.tsx @@ -44,7 +44,7 @@ const conditionSplitter = (condition: BooleanExpression): string[] => { function EditableBinaryMeta(props: EditableBinaryMetaProps) { const { binaryMeta, changeMeta } = props; - const { condition, maxXp } = binaryMeta; + const { condition, targetCount } = binaryMeta; const joiners: string[] = []; const conditions: string[] = []; @@ -72,7 +72,8 @@ function EditableBinaryMeta(props: EditableBinaryMetaProps) { changeMeta({ ...binaryMeta, condition: condition }); }; - const changeMaxXp = (maxXp: number) => changeMeta({ ...binaryMeta, maxXp: maxXp }); + const changeTargetCount = (targetCount: number) => + changeMeta({ ...binaryMeta, targetCount: targetCount }); // Adds the and/or, adds the condition to be edited const addCondition = () => { @@ -148,14 +149,14 @@ function EditableBinaryMeta(props: EditableBinaryMetaProps) { return ( <> - + {generateConditions()} diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx index b1f451e33f..4f24befef4 100644 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx @@ -25,9 +25,7 @@ const conditionRenderer: ItemRenderer = (condition, { handleCli function EditableEventMeta(props: EditableEventMetaProps) { const { changeMeta, eventMeta } = props; - const { eventNames, targetCount, maxXp, condition } = eventMeta; - - const changeMaxXp = (maxXp: number) => changeMeta({ ...eventMeta, maxXp: maxXp }); + const { eventNames, targetCount, condition } = eventMeta; const changeTargetCount = (targetCount: number) => changeMeta({ ...eventMeta, targetCount: targetCount }); @@ -51,7 +49,7 @@ function EditableEventMeta(props: EditableEventMetaProps) { const generateEventNames = () => { return eventNames.map((eventName, index) => ( - +
    diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 3084c870af..58b4ff105a 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -1,9 +1,10 @@ import { GoalDefinition, GoalType } from '../../../features/achievement/AchievementTypes'; export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ - maxXp: goal.meta.type === GoalType.ASSESSMENT ? 0 : goal.meta.maxXp, + targetCount: goal.meta.type === GoalType.ASSESSMENT ? 1 : goal.meta.targetCount, meta: goal.meta, text: goal.text, type: goal.meta.type, - uuid: goal.uuid + uuid: goal.uuid, + achievementUuids: goal.achievementUuids }); diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 738a9a08f3..448dfc84d0 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -3,11 +3,13 @@ import { v4 } from 'uuid'; import { showDangerMessage } from '../../../commons/utils/NotificationsHelper'; import { + AchievementAbility, AchievementGoal, AchievementItem, AchievementStatus, defaultGoalProgress, - GoalDefinition + GoalDefinition, + GoalType } from '../../../features/achievement/AchievementTypes'; import { isExpired } from './DateHelper'; @@ -17,7 +19,6 @@ import { isExpired } from './DateHelper'; * @param {AchievementItem} achievement the achievement item * @param {Date | undefined} displayDeadline deadline displayed on the achievement card * @param {number} xp attained XP of the achievement - * @param {number} maxXp maximum attainable XP of the achievement * @param {number} progressFrac progress percentage in fraction. It is always between 0 to 1, both inclusive. * @param {AchievementStatus} status the achievement status * @param {Set} children a set of immediate prerequisites uuid @@ -27,7 +28,6 @@ class AchievementNode { public achievement: AchievementItem; public displayDeadline?: Date; public xp: number; - public maxXp: number; public progressFrac: number; public status: AchievementStatus; public children: Set; @@ -39,7 +39,6 @@ class AchievementNode { this.achievement = achievement; this.displayDeadline = deadline; this.xp = 0; - this.maxXp = 0; this.progressFrac = 0; this.status = AchievementStatus.ACTIVE; this.children = new Set(prerequisiteUuids); @@ -67,6 +66,37 @@ class AchievementInferencer { this.processGoals(); } + /** + * Invalid Goal for the getters to return if the goal does not exist in the goalList + */ + private invalidGoal: AchievementGoal = { + uuid: 'invalid', + text: 'invalid', + achievementUuids: [], + meta: {type: GoalType.MANUAL, targetCount: 0}, + count: 0, + targetCount: 0, + completed: false + } + + /** + * Invalid Achievement for the getters to return if the goal does not exist in the goalList + */ + private invalidAchievement: AchievementItem = { + uuid: 'invalid', + title: 'invalid', + ability: AchievementAbility.EXPLORATION, + xp: 0, + deadline: undefined, + release: undefined, + isTask: false, + position: 0, + prerequisiteUuids: [], + goalUuids: [], + cardBackground: 'invalid', + view: {coverImage: 'invalid', description: 'invalid', completionText: 'invalid'} + } + /** * Returns an array of AchievementItem */ @@ -74,6 +104,19 @@ class AchievementInferencer { return [...this.nodeList.values()].map(node => node.achievement); } + /** + * Returns an array of AchievementItem that have been released + */ + public getAllReleasedAchievements() { + return [...this.nodeList.values()] + .filter(node => node.status !== AchievementStatus.UNRELEASED) + .map(node => node.achievement); + } + + public getAllCompletedAchievements() { + return [...this.nodeList.values()].filter(node => node.status === AchievementStatus.COMPLETED).map(node => node.achievement); + } + /** * Returns an array of achievementUuid */ @@ -104,11 +147,13 @@ class AchievementInferencer { /** * Returns the AchievementItem + * Returns an invalid achievement item if the achievement is not in the map * * @param uuid Achievement Uuid */ public getAchievement(uuid: string) { - return this.nodeList.get(uuid)!.achievement; + const node = this.nodeList.get(uuid); + return node ? node.achievement : this.invalidAchievement; } /** @@ -120,11 +165,12 @@ class AchievementInferencer { /** * Returns the AchievementGoal + * Returns an invalid goal if the goal is not in the map * * @param uuid Goal Uuid */ public getGoal(uuid: string) { - return this.goalList.get(uuid)!; + return this.goalList.get(uuid) || this.invalidGoal; } /** @@ -133,7 +179,7 @@ class AchievementInferencer { * @param uuid Goal Uuid */ public getGoalDefinition(uuid: string) { - return this.goalList.get(uuid)! as GoalDefinition; + return this.getGoal(uuid) as GoalDefinition; } /** @@ -142,7 +188,7 @@ class AchievementInferencer { * @param uuid Achievement Uuid */ public getAchievementPositionByUuid(uuid: string) { - return this.nodeList.get(uuid)!.achievement.position; + return this.getAchievement(uuid).position; } /** @@ -167,6 +213,33 @@ class AchievementInferencer { this.goalsToDelete = []; } + /** + * Returns an array of achievements that use the goal + * + * @param goalUuid UUID of the goal in question + */ + public getAchievementsByGoal(goalUuid: string) { + return this.getGoal(goalUuid).achievementUuids; + } + + /** + * Returns true if the goal is invalid + * + * @param goal An AchievementGoal + */ + public isInvalidGoal(goal: AchievementGoal) { + return goal === this.invalidGoal; + } + + /** + * Returns true if the achievement is invalid + * + * @param achievement An AchievementItem + */ + public isInvalidAchievement (achievement: AchievementItem) { + return achievement === this.invalidAchievement; + } + /** * Inserts a new AchievementItem into the Inferencer, * then returns the newly assigned achievementUuid @@ -189,6 +262,24 @@ class AchievementInferencer { return newUuid; } + /** + * Inserts a new AchievementItem into the Inferencer, + * then returns the newly assigned achievementUuid. + * Used for inserting assessment achievements on the fly. + * + * @param achievement the AchievementItem + */ + public insertFakeAchievement(achievement: AchievementItem) { + // and insert it into nodeList + this.nodeList.set(achievement.uuid, new AchievementNode(achievement)); + + // finally, process the nodeList + this.processNodes(); + this.normalizePositions(achievement.uuid, achievement.position); + + return achievement.uuid; + } + /** * Inserts a new GoalDefinition into the Inferencer, * then returns the newly assigned goalUuid @@ -210,6 +301,29 @@ class AchievementInferencer { return newUuid; } + /** + * Inserts a new GoalDefinition into the Inferencer, + * then returns the newly assigned goalUuid + * Used for inserting assessment goals on the fly + * + * @param definition the GoalDefinition + * @param complete whether the goal should be marked as completed or not + */ + public insertFakeGoalDefinition(definition: GoalDefinition, complete: boolean) { + // then assign the new unique uuid by overwriting the goal item supplied by param + // and insert it into goalList + if (complete) { + this.goalList.set(definition.uuid, { ...definition, count: 1, targetCount: 1, completed: true}); + } else { + this.goalList.set(definition.uuid, { ...definition, count: 0, targetCount: 1, completed: false}); + } + + // finally, process the goalList + this.processGoals(); + + return definition.uuid + } + /** * Updates the AchievementItem in the Inferencer * @@ -255,9 +369,14 @@ class AchievementInferencer { return new AchievementNode(node.achievement); }; - // first, remove achievement from node list + // first, remove the references to this achievement from the goals + this.getAchievement(targetUuid).goalUuids.forEach(goalUuid => { + const goal = this.getGoal(goalUuid); + goal.achievementUuids = goal.achievementUuids.filter(uuid => uuid !== targetUuid); + }); + // then, remove achievement from node list this.nodeList.delete(targetUuid); - // then, remove reference of the target in other achievement's prerequisite + // finally, remove reference of the target in other achievement's prerequisite this.nodeList.forEach((node, uuid) => { if (hasTarget(node)) { this.nodeList.set(uuid, sanitizeNode(node)); @@ -302,24 +421,55 @@ class AchievementInferencer { } /** - * Returns an array of achievementUuid that isTask + * Returns an array of achievementUuid that isTask or is completed */ public listTaskUuids() { return this.getAllAchievements() - .filter(achievement => achievement.isTask) + .filter(achievement => achievement.isTask || this.isCompleted(achievement)) .map(task => task.uuid); } + /** - * Returns an array of achievementId that isTask sorted by position + * Returns an array of achievementId that isTask or is completed sorted by position */ - public listSortedTaskUuids() { + public listSortedTaskUuids() { return this.getAllAchievements() - .filter(achievement => achievement.isTask) + .filter(achievement => achievement.isTask || this.isCompleted(achievement)) .sort((taskA, taskB) => taskA.position - taskB.position) .map(sortedTask => sortedTask.uuid); } + /** + * Returns an array of achievementId that isTask or is completed sorted by position + */ + public listSortedReleasedTaskUuids() { + return this.getAllReleasedAchievements() + .filter(achievement => achievement.isTask || this.isCompleted(achievement)) + .sort((taskA, taskB) => taskA.position - taskB.position) + .map(sortedTask => sortedTask.uuid); + } + + /** + * Returns whether an achievement is completed or not. + * + * NOTE: It might be better (more efficient) to simply have a completed proporty on each achievement. + */ + private isCompleted(achievement: AchievementItem) { + const goalLength = achievement.goalUuids.length; + // an achievement with no goals should not be considered complete + if (goalLength === 0) { + return false; + } + // if any of the goals are not complete, return false + for (let i = 0; i < goalLength; i++) { + if (!this.getGoal(achievement.goalUuids[i]).completed) { + return false; + } + } + return true; + } + /** * Returns an array of AchievementGoal which belongs to the achievement * @@ -360,7 +510,7 @@ class AchievementInferencer { * @param text goalUuid */ public getTextByUuid(uuid: string) { - return this.goalList.get(uuid)?.text; + return this.getGoal(uuid).text; } /** @@ -378,7 +528,7 @@ class AchievementInferencer { * @param text achievementUuid */ public getTitleByUuid(uuid: string) { - return this.nodeList.get(uuid)?.achievement.title; + return this.getAchievement(uuid).title; } /** @@ -387,26 +537,14 @@ class AchievementInferencer { * @param uuid Achievement Uuid */ public getAchievementXp(uuid: string) { - return this.nodeList.has(uuid) ? this.nodeList.get(uuid)!.xp : 0; + return this.getAchievement(uuid).xp; } /** - * Returns the maximum attainable XP from the achievement - * - * @param uuid Achievement Uuid - */ - public getAchievementMaxXp(uuid: string) { - return this.nodeList.has(uuid) ? this.nodeList.get(uuid)!.maxXp : 0; - } - - /** - * Returns total XP earned from all goals - * - * Note: Goals that do not belong to any achievement is also added into the total XP - * calculation + * Returns total XP earned from all achievements */ public getTotalXp() { - return this.getAllGoals().reduce((totalXp, goal) => totalXp + goal.xp, 0); + return this.getAllCompletedAchievements().reduce((totalXp, achievement) => totalXp + achievement.xp, 0); } /** @@ -501,7 +639,7 @@ class AchievementInferencer { this.generateDescendant(node); this.generateDisplayDeadline(node); - this.generateXpAndMaxXp(node); + this.generateXp(node); this.generateProgressFrac(node); this.generateStatus(node); }); @@ -602,14 +740,17 @@ class AchievementInferencer { } /** - * Calculates the achievement attained XP and maximum attainable XP + * Calculates the achievement attained XP * * @param node the AchievementNode */ - private generateXpAndMaxXp(node: AchievementNode) { + private generateXp(node: AchievementNode) { const { goalUuids } = node.achievement; - node.xp = goalUuids.reduce((xp, goalUuid) => xp + this.getGoal(goalUuid).xp, 0); - node.maxXp = goalUuids.reduce((maxXp, goalUuid) => maxXp + this.getGoal(goalUuid).maxXp, 0); + const allGoalsCompleted = goalUuids.reduce( + (completion, goalUuid) => completion && this.getGoal(goalUuid).completed, + true + ); + node.xp = allGoalsCompleted ? node.achievement.xp : 0; } /** @@ -619,9 +760,13 @@ class AchievementInferencer { */ private generateProgressFrac(node: AchievementNode) { const { goalUuids } = node.achievement; - const xp = goalUuids.reduce((xp, goalUuid) => xp + this.getGoal(goalUuid).xp, 0); - - node.progressFrac = node.maxXp === 0 ? 0 : Math.min(xp / node.maxXp, 1); + if (goalUuids.length === 0) { + node.progressFrac = 0; + } else { + const num = goalUuids.reduce((count, goalUuid) => count + this.getGoal(goalUuid).count, 0); + const denom = goalUuids.reduce((count, goalUuid) => count + this.getGoal(goalUuid).targetCount, 0); + node.progressFrac = Math.min(denom === 0 ? 0 : num / denom, 1); + } } /** @@ -642,13 +787,16 @@ class AchievementInferencer { .map(goalUuid => this.getGoal(goalUuid).completed) .reduce((result, goalCompleted) => result && goalCompleted, true); + const isReleased = node.achievement.release === undefined || isExpired(node.achievement.release); const hasUnexpiredDeadline = !isExpired(node.displayDeadline); node.status = achievementCompleted ? AchievementStatus.COMPLETED - : hasUnexpiredDeadline - ? AchievementStatus.ACTIVE - : AchievementStatus.EXPIRED; + : isReleased + ? hasUnexpiredDeadline + ? AchievementStatus.ACTIVE + : AchievementStatus.EXPIRED + : AchievementStatus.UNRELEASED; } /** diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index e4e962a374..62eb4d93d7 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -16,6 +16,7 @@ const testAchievement: AchievementItem = { uuid: '0', title: 'Test Achievement', ability: AchievementAbility.CORE, + xp: 0, isTask: false, prerequisiteUuids: [], goalUuids: [], @@ -33,12 +34,13 @@ const testAchievement: AchievementItem = { const testGoal: AchievementGoal = { uuid: '0', text: 'Test Goal', + achievementUuids: [], meta: { type: GoalType.MANUAL, - maxXp: 0 + targetCount: 0 }, - xp: 0, - maxXp: 0, + count: 0, + targetCount: 0, completed: false }; @@ -463,6 +465,7 @@ describe('Achievement Prerequisite System', () => { }); }); +/* To be restored when XP system is fixed describe('Achievement XP System', () => { const testAchievement1: AchievementItem = { ...testAchievement, @@ -502,6 +505,7 @@ describe('Achievement XP System', () => { expect(inferencer.getProgressFrac('101')).toBe(0); }); }); +*/ describe('Achievement Display Deadline', () => { const expiredDeadline = new Date(1920, 1, 1); diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index d04ba0340f..37a0e1586d 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -12,6 +12,7 @@ import { GoalType } from '../../../features/achievement/AchievementTypes'; import { store } from '../../../pages/createStore'; +import { showSuccessMessage } from '../../utils/NotificationsHelper'; import AchievementInferencer from './AchievementInferencer'; function eventConditionSatisfied(meta: EventMeta): boolean { @@ -68,42 +69,53 @@ const goalIncludesEvent = (goal: AchievementGoal, eventName: EventType) => { }; export function processEvent(eventName: EventType, increment: number = 1) { + // by default, userId should be the current state's one + const userId = store.getState().session.userId; + // just in case userId is still not defined + if (!userId) { + return; + } + let goals = inferencer.getAllGoals(); - // if the state has goals, enter the function body + // if the inferencer has goals, enter the function body if (goals[0]) { goals = goals.filter(goal => goalIncludesEvent(goal, eventName)); const computeCompleted = (goal: AchievementGoal): boolean => { // all goals that are input as arguments are eventGoals const meta = goal.meta as EventMeta; - if (!goal.completed && goal.xp + increment >= meta.targetCount) { - // replace with a nice notification in the future - alert('Completed acheivement: ' + goal.text); + + // if the goal just became completed + if (!goal.completed && goal.count + increment >= meta.targetCount) { + goal.completed = true; + const parentAchievements = inferencer.getAchievementsByGoal(goal.uuid); + parentAchievements.forEach(uuid => { + const completed = inferencer + .getAchievement(uuid) + .goalUuids.map(goalUuid => inferencer.getGoal(goalUuid).completed) + .reduce((completion, goalCompletion) => completion && goalCompletion, true); + if (completed) { + showSuccessMessage('Completed acheivement: ' + inferencer.getAchievement(uuid).title); + } + }); return true; } else { return goal.completed; } }; - const userId = store.getState().session.userId; - // not sure what to do in this case... - if (!userId) { - return; - } - - // future changes: xp should be count! goals.forEach(goal => { if (eventConditionSatisfied(goal.meta as EventMeta)) { // edit the version that is on the state - goal.completed = computeCompleted(goal); - goal.xp = goal.xp + increment; + computeCompleted(goal); + goal.count = goal.count + increment; // send the update request to the backend const progress: GoalProgress = { uuid: goal.uuid, - xp: goal.xp, // user gets all of this xp, even if its not complete - maxXp: goal.maxXp, // when complete, the user gets the xp + count: goal.count, // user gets all of this xp, even if its not complete + targetCount: goal.targetCount, // when complete, the user gets the xp // check for completion using counter that gets incremented completed: goal.completed }; @@ -121,7 +133,7 @@ export function processEvent(eventName: EventType, increment: number = 1) { ); processEvent(eventName, increment); }; - + if (!store.getState().achievement.goals[0]) { // ensure that the next function call has updated XP values store.dispatch(getOwnGoals()); @@ -137,4 +149,4 @@ export function processEvent(eventName: EventType, increment: number = 1) { retry(); } } -} +} \ No newline at end of file diff --git a/src/commons/achievement/view/AchievementViewGoal.tsx b/src/commons/achievement/view/AchievementViewGoal.tsx index b268c5b1df..da8d4d9c56 100644 --- a/src/commons/achievement/view/AchievementViewGoal.tsx +++ b/src/commons/achievement/view/AchievementViewGoal.tsx @@ -1,3 +1,5 @@ +import { ProgressBar } from '@blueprintjs/core'; + import { AchievementGoal } from '../../../features/achievement/AchievementTypes'; type AchievementViewGoalProps = { @@ -10,16 +12,28 @@ type AchievementViewGoalProps = { * @param goal an array of goalUuid */ const mapGoalToJSX = (goal: AchievementGoal) => { - const { uuid, text, maxXp, xp } = goal; + const { uuid, text, targetCount, count, completed } = goal; + const frac = Math.min(targetCount === 0 ? 0 : count / targetCount, 1); return (

    - {xp} / {maxXp} XP + {count} / {targetCount} +

    +
    +
    +

    + {text}

    +
    -

    {text}

    ); }; diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 2c12ba0b13..9db0d698c3 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -175,7 +175,8 @@ export const defaultDashboard: DashboardState = { export const defaultAchievement: AchievementState = { achievements: [], - goals: [] + goals: [], + users: [] }; export const defaultPlayground: PlaygroundState = { diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index fb1de39bcb..8fd77b06d5 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -11,6 +11,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '0', title: 'Rune Master', ability: AchievementAbility.CORE, + xp: 100, isTask: true, position: 1, prerequisiteUuids: ['2', '1'], @@ -29,6 +30,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '1', title: 'Beyond the Second Dimension', ability: AchievementAbility.CORE, + xp: 100, deadline: new Date(2020, 7, 6, 12, 30, 0), isTask: false, position: 0, @@ -48,6 +50,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '2', title: 'Colorful Carpet', ability: AchievementAbility.CORE, + xp: 100, deadline: new Date(2020, 7, 8, 9, 0, 0), isTask: false, position: 0, @@ -67,6 +70,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '3', title: '', ability: AchievementAbility.CORE, + xp: 100, isTask: false, position: 0, prerequisiteUuids: [], @@ -82,6 +86,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '4', title: 'Curve Wizard', ability: AchievementAbility.CORE, + xp: 100, deadline: new Date(2020, 8, 15, 0, 0, 0), isTask: true, position: 4, @@ -101,6 +106,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '5', title: 'Curve Introduction', ability: AchievementAbility.CORE, + xp: 100, deadline: new Date(2020, 7, 28, 0, 0, 0), isTask: false, position: 0, @@ -120,6 +126,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '6', title: 'Curve Manipulation', ability: AchievementAbility.CORE, + xp: 100, deadline: new Date(2020, 8, 5, 0, 0, 0), isTask: false, position: 0, @@ -139,6 +146,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '21', title: 'The Source-rer', ability: AchievementAbility.EFFORT, + xp: 100, deadline: new Date(2020, 7, 21, 0, 0, 0), isTask: true, position: 3, @@ -158,6 +166,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '8', title: 'Power of Friendship', ability: AchievementAbility.COMMUNITY, + xp: 100, isTask: true, position: 2, prerequisiteUuids: ['9'], @@ -176,6 +185,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '9', title: 'Piazza Guru', ability: AchievementAbility.COMMUNITY, + xp: 100, isTask: false, position: 0, prerequisiteUuids: [], @@ -194,6 +204,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '16', title: "That's the Spirit", ability: AchievementAbility.EXPLORATION, + xp: 100, isTask: true, position: 5, prerequisiteUuids: [], @@ -212,6 +223,7 @@ export const mockAchievements: AchievementItem[] = [ uuid: '13', title: 'Kool Kidz', ability: AchievementAbility.FLEX, + xp: 100, isTask: true, position: 6, prerequisiteUuids: [], @@ -237,35 +249,38 @@ export const mockGoals: AchievementGoal[] = [ { event: EventTypes.ASSESSMENT_GRADING, restriction: 'M2A' }, { event: EventTypes.ASSESSMENT_GRADING, restriction: 'M2B' } ), - maxXp: 100 + targetCount: 100 }, - xp: 0, - maxXp: 100, - completed: false + count: 0, + targetCount: 100, + completed: false, + achievementUuids: ['0'] }, { uuid: '1', text: 'XP earned from Beyond the Second Dimension Mission', meta: { type: GoalType.ASSESSMENT, - assessmentNumber: 'M2B', + assessmentNumber: 1, requiredCompletionFrac: 0.5 }, - xp: 213, - maxXp: 250, - completed: true + count: 213, + targetCount: 250, + completed: true, + achievementUuids: ['1'] }, { uuid: '2', text: 'XP earned from Colorful Carpet Mission', meta: { type: GoalType.ASSESSMENT, - assessmentNumber: 'M2A', + assessmentNumber: 2, requiredCompletionFrac: 0.8 }, - xp: 0, - maxXp: 250, - completed: false + count: 0, + targetCount: 250, + completed: false, + achievementUuids: ['2'] }, { uuid: '3', @@ -282,35 +297,38 @@ export const mockGoals: AchievementGoal[] = [ restriction: 'M4A' } ), - maxXp: 100 + targetCount: 100 }, - xp: 0, - maxXp: 100, - completed: false + count: 0, + targetCount: 100, + completed: false, + achievementUuids: ['4'] }, { uuid: '4', text: 'XP earned from Curve Introduction Mission', meta: { type: GoalType.ASSESSMENT, - assessmentNumber: 'M3', + assessmentNumber: 3, requiredCompletionFrac: 0.5 }, - xp: 178, - maxXp: 250, - completed: true + count: 178, + targetCount: 250, + completed: true, + achievementUuids: ['5'] }, { uuid: '5', text: 'XP earned from Curve Manipulation Mission', meta: { type: GoalType.ASSESSMENT, - assessmentNumber: 'M4A', + assessmentNumber: 4, requiredCompletionFrac: 0.8 }, - xp: 191, - maxXp: 250, - completed: false + count: 191, + targetCount: 250, + completed: false, + achievementUuids: ['6'] }, { uuid: '16', @@ -321,45 +339,49 @@ export const mockGoals: AchievementGoal[] = [ event: EventTypes.ASSESSMENT_SUBMISSION, restriction: 'P3' }, - maxXp: 100 + targetCount: 100 }, - xp: 100, - maxXp: 100, - completed: true + count: 100, + targetCount: 100, + completed: true, + achievementUuids: ['21'] }, { uuid: '18', text: 'XP earned from Source 3 Path', meta: { type: GoalType.ASSESSMENT, - assessmentNumber: 'P3', + assessmentNumber: 5, requiredCompletionFrac: 1 }, - xp: 300, - maxXp: 300, - completed: true + count: 300, + targetCount: 300, + completed: true, + achievementUuids: ['21'] }, { uuid: '8', text: 'Each Top Voted answer in Piazza gives 10 XP', meta: { type: GoalType.MANUAL, - maxXp: 100 + targetCount: 100 }, - xp: 40, - maxXp: 100, - completed: false + count: 40, + targetCount: 100, + completed: false, + achievementUuids: ['9'] }, { uuid: '14', text: 'Submit 1 PR to Source Academy Github', meta: { type: GoalType.MANUAL, - maxXp: 100 + targetCount: 100 }, - xp: 100, - maxXp: 100, - completed: true + count: 100, + targetCount: 100, + completed: true, + achievementUuids: ['16'] }, { uuid: '11', @@ -367,10 +389,11 @@ export const mockGoals: AchievementGoal[] = [ meta: { type: GoalType.BINARY, condition: false, - maxXp: 100 + targetCount: 100 }, - xp: 0, - maxXp: 100, - completed: false + count: 0, + targetCount: 100, + completed: false, + achievementUuids: ['13'] } ]; diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index 5824ec917c..2c4e2cc959 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -9,6 +9,7 @@ import { GET_ACHIEVEMENTS, GET_GOALS, GET_OWN_GOALS, + GET_USERS, REMOVE_ACHIEVEMENT, REMOVE_GOAL, UPDATE_GOAL_PROGRESS @@ -21,6 +22,7 @@ import { editAchievement, editGoal, getAchievements, + getAllUsers, getGoals, getOwnGoals, removeAchievement, @@ -137,6 +139,19 @@ export default function* AchievementSaga(): SagaIterator { } }); + yield takeEvery(GET_USERS, function* (action: ReturnType) { + const tokens = yield select((state: OverallState) => ({ + accessToke: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + + const users = yield call(getAllUsers, tokens); + + if (users) { + yield put(actions.saveUsers(users)); + } + }); + yield takeEvery( REMOVE_ACHIEVEMENT, function* (action: ReturnType) { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index e7daec21e4..7c0dc73c25 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -15,6 +15,7 @@ import { AchievementAbility, AchievementGoal, AchievementItem, + AchievementUser, GoalDefinition, GoalMeta, GoalProgress @@ -142,6 +143,7 @@ export const getAchievements = async (tokens: Tokens): Promise => { + const resp = await request('admin/users', 'GET', { + ...tokens, + shouldRefresh: true + }); + + if (!resp || !resp.ok) { + return null; // invalid accessToken _and_ refreshToken + } + + const users = await resp.json(); + + return users.map( + (user: any) => + ({ + name: user.name, + userId: user.userId, + group: user.group + } as AchievementUser) + ); +}; + /** * PUT /admin/achievements */ diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index e5f10e9b55..ca6046c358 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -22,8 +22,10 @@ import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select, take } from 'redux-saga/effects'; import * as Sourceror from 'sourceror'; +import { EventType } from '../../features/achievement/AchievementTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { DeviceSession } from '../../features/remoteExecution/RemoteExecutionTypes'; +import { processEvent } from '../achievement/utils/eventHandler'; import { OverallState, styliseSublanguage } from '../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../application/types/ExternalTypes'; import { @@ -116,6 +118,9 @@ export default function* WorkspaceSaga(): SagaIterator { globals }; + // achievement: runCode + processEvent(EventType.RUNCODE); + if (remoteExecutionSession && remoteExecutionSession.workspace === workspaceLocation) { yield put(actions.remoteExecRun(editorCode)); } else { diff --git a/src/features/achievement/AchievementActions.ts b/src/features/achievement/AchievementActions.ts index 89678a89b8..aa5d981f6f 100644 --- a/src/features/achievement/AchievementActions.ts +++ b/src/features/achievement/AchievementActions.ts @@ -3,6 +3,7 @@ import { action } from 'typesafe-actions'; import { AchievementGoal, AchievementItem, + AchievementUser, BULK_UPDATE_ACHIEVEMENTS, BULK_UPDATE_GOALS, EDIT_ACHIEVEMENT, @@ -10,12 +11,14 @@ import { GET_ACHIEVEMENTS, GET_GOALS, GET_OWN_GOALS, + GET_USERS, GoalDefinition, GoalProgress, REMOVE_ACHIEVEMENT, REMOVE_GOAL, SAVE_ACHIEVEMENTS, SAVE_GOALS, + SAVE_USERS, UPDATE_GOAL_PROGRESS } from './AchievementTypes'; @@ -35,6 +38,8 @@ export const getGoals = (studentId: number) => action(GET_GOALS, studentId); export const getOwnGoals = () => action(GET_OWN_GOALS); +export const getUsers = () => action(GET_USERS); + export const removeAchievement = (uuid: string) => action(REMOVE_ACHIEVEMENT, uuid); export const removeGoal = (uuid: string) => action(REMOVE_GOAL, uuid); @@ -52,5 +57,11 @@ export const saveAchievements = (achievements: AchievementItem[]) => */ export const saveGoals = (goals: AchievementGoal[]) => action(SAVE_GOALS, goals); +/* + Note: This updates the frontend Achievement Redux store. + Please refer to AchievementReducer to find out more. +*/ +export const saveUsers = (users: AchievementUser[]) => action(SAVE_USERS, users); + export const updateGoalProgress = (studentId: number, progress: GoalProgress) => action(UPDATE_GOAL_PROGRESS, { studentId, progress }); diff --git a/src/features/achievement/AchievementReducer.ts b/src/features/achievement/AchievementReducer.ts index 94e4aeeefd..33d4faaea3 100644 --- a/src/features/achievement/AchievementReducer.ts +++ b/src/features/achievement/AchievementReducer.ts @@ -2,7 +2,7 @@ import { Reducer } from 'redux'; import { defaultAchievement } from '../../commons/application/ApplicationTypes'; import { SourceActionType } from '../../commons/utils/ActionsHelper'; -import { AchievementState, SAVE_ACHIEVEMENTS, SAVE_GOALS } from './AchievementTypes'; +import { AchievementState, SAVE_ACHIEVEMENTS, SAVE_GOALS, SAVE_USERS } from './AchievementTypes'; export const AchievementReducer: Reducer = ( state = defaultAchievement, @@ -19,6 +19,11 @@ export const AchievementReducer: Reducer = ( ...state, goals: action.payload }; + case SAVE_USERS: + return { + ...state, + users: action.payload + } default: return state; } diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index ae7529aef6..f677d54740 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -7,10 +7,12 @@ export const EDIT_GOAL = 'EDIT_GOAL'; export const GET_ACHIEVEMENTS = 'GET_ACHIEVEMENTS'; export const GET_GOALS = 'GET_GOALS'; export const GET_OWN_GOALS = 'GET_OWN_GOALS'; +export const GET_USERS = 'GET_USERS'; export const REMOVE_ACHIEVEMENT = 'REMOVE_ACHIEVEMENT'; export const REMOVE_GOAL = 'REMOVE_GOAL'; export const SAVE_ACHIEVEMENTS = 'SAVE_ACHIEVEMENTS'; export const SAVE_GOALS = 'SAVE_GOALS'; +export const SAVE_USERS = 'SAVE_USERS'; export const UPDATE_GOAL_PROGRESS = 'UPDATE_GOAL_PROGRESS'; export enum AchievementAbility { @@ -24,7 +26,8 @@ export enum AchievementAbility { export enum AchievementStatus { ACTIVE = 'ACTIVE', // deadline not over and not completed COMPLETED = 'COMPLETED', // completed, regardless of deadline - EXPIRED = 'EXPIRED' // deadline over and not completed + EXPIRED = 'EXPIRED', // deadline over and not completed + UNRELEASED = 'UNRELEASED' // release date not reached yet } export enum FilterStatus { @@ -39,6 +42,7 @@ export enum FilterStatus { * @param {string} uuid unique uuid of the achievement item * @param {string} title title of the achievement * @param {AchievementAbility} ability ability of the achievement, string enum + * @param {number} xp the xp gained when completing the achievement * @param {Date} deadline Optional, the deadline of the achievement * @param {Date} release Optional, the release date of the achievement * @param {boolean} isTask if true, the achievement is rendered as an achievement task @@ -52,6 +56,7 @@ export type AchievementItem = { uuid: string; title: string; ability: AchievementAbility; + xp: number; deadline?: Date; release?: Date; isTask: boolean; @@ -69,11 +74,13 @@ export type AchievementGoal = GoalDefinition & GoalProgress; * * @param {string} uuid unique uuid of the goal * @param {string} text goal description + * @param {string[]} achievementUuids an array of achievement uuids that use this goal * @param {GoalMeta} meta contains meta data relevant to the goal type */ export type GoalDefinition = { uuid: string; text: string; + achievementUuids: string[]; meta: GoalMeta; }; @@ -87,20 +94,20 @@ export type GoalDefinition = { */ export type GoalProgress = { uuid: string; - xp: number; - maxXp: number; + count: number; + targetCount: number; completed: boolean; }; export const defaultGoalProgress = { - xp: 0, - maxXp: 0, + count: 0, + targetCount: 1, completed: false }; export enum GoalType { - ASSESSMENT = 'Assessment', - BINARY = 'Binary', + ASSESSMENT = 'Assessment (unsupported)', + BINARY = 'Binary (unsupported)', MANUAL = 'Manual', EVENT = 'Event' } @@ -127,19 +134,19 @@ export type GoalMeta = AssessmentMeta | BinaryMeta | ManualMeta | EventMeta; export type AssessmentMeta = { type: GoalType.ASSESSMENT; - assessmentNumber: string; // e.g. 'M1A', 'P2' + assessmentNumber: number; requiredCompletionFrac: number; // between [0..1] }; export type BinaryMeta = { type: GoalType.BINARY; condition: BooleanExpression; - maxXp: number; + targetCount: number; }; export type ManualMeta = { type: GoalType.MANUAL; - maxXp: number; + targetCount: number; }; export type EventMeta = { @@ -147,7 +154,6 @@ export type EventMeta = { eventNames: EventType[]; targetCount: number; condition: Condition; - maxXp: number; }; /** @@ -163,7 +169,14 @@ export type AchievementView = { completionText: string; }; +export type AchievementUser = { + name: string; + userId: number; + group: string; +} + export type AchievementState = { achievements: AchievementItem[]; goals: AchievementGoal[]; + users: AchievementUser[]; }; diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 3d06ee8928..71dfc1dfd3 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -1,6 +1,7 @@ import { IconNames } from '@blueprintjs/icons'; import { useEffect, useState } from 'react'; import { Role } from 'src/commons/application/ApplicationTypes'; +import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; import AchievementFilter from '../../../commons/achievement/AchievementFilter'; import AchievementManualEditor from '../../../commons/achievement/AchievementManualEditor'; @@ -9,12 +10,14 @@ import AchievementTask from '../../../commons/achievement/AchievementTask'; import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import Constants from '../../../commons/utils/Constants'; -import { AchievementContext } from '../../../features/achievement/AchievementConstants'; -import { FilterStatus, GoalProgress } from '../../../features/achievement/AchievementTypes'; +import { AchievementContext, cardBackgroundUrl, coverImageUrl } from '../../../features/achievement/AchievementConstants'; +import { AchievementAbility, AchievementUser, FilterStatus, GoalProgress, GoalType } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { + fetchAssessmentOverviews: () => void; getAchievements: () => void; getOwnGoals: () => void; + getUsers: () => void; updateGoalProgress: (studentId: number, progress: GoalProgress) => void; }; @@ -23,6 +26,8 @@ export type StateProps = { inferencer: AchievementInferencer; name?: string; role?: Role; + assessmentOverviews?: AssessmentOverview[]; + users: AchievementUser[]; }; /** @@ -47,7 +52,8 @@ export const generateAchievementTasks = ( )); function Dashboard(props: DispatchProps & StateProps) { - const { group, getAchievements, getOwnGoals, updateGoalProgress, inferencer, name, role } = props; + const { getAchievements, getOwnGoals, getUsers, updateGoalProgress, fetchAssessmentOverviews, + group, inferencer, name, role, assessmentOverviews, users } = props; /** * Fetch the latest achievements and goals from backend when the page is rendered @@ -59,6 +65,64 @@ function Dashboard(props: DispatchProps & StateProps) { } }, [getAchievements, getOwnGoals]); + if (name && role && !assessmentOverviews) { + // If assessment overviews are not loaded, fetch them + fetchAssessmentOverviews(); + } + + // one goal for submit, one goal for graded + assessmentOverviews?.forEach(assessmentOverview => { + const idString = assessmentOverview.id.toString(); + if (!inferencer.hasAchievement(idString)) { + // Goal for assessment submission + inferencer.insertFakeGoalDefinition( + { uuid: idString + '0', + text: `Submitted ${assessmentOverview.category.toLowerCase()}`, + achievementUuids: [idString], + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, + requiredCompletionFrac: 0 + } + }, assessmentOverview.status === 'submitted' + ) + // Goal for assessment grading + inferencer.insertFakeGoalDefinition( + { uuid: idString + '1', + text: `Graded ${assessmentOverview.category.toLowerCase()}`, + achievementUuids: [idString], + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, + requiredCompletionFrac: 0 + } + }, assessmentOverview.gradingStatus === 'graded' + ) + // Would like a goal for early submission, but that seems to be hard to get from the overview + inferencer.insertFakeAchievement( + { uuid: idString, + title: assessmentOverview.title, + ability: assessmentOverview.category === 'Mission' || assessmentOverview.category === 'Path' + ? AchievementAbility.CORE + : AchievementAbility.EFFORT, + xp: assessmentOverview.gradingStatus === 'graded' ? assessmentOverview.xp : assessmentOverview.maxXp, + deadline: new Date(assessmentOverview.closeAt), + release: new Date(assessmentOverview.openAt), + isTask: assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, + position: -1, // always appears on top + prerequisiteUuids: [], + goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete + cardBackground: `${cardBackgroundUrl}/default.png`, + view: { + coverImage: `${coverImageUrl}/default.png`, + description: assessmentOverview.shortSummary, + completionText: `Grade: ${assessmentOverview.grade} / ${assessmentOverview.maxGrade}` + } + } + ) + } + }); + const filterState = useState(FilterStatus.ALL); const [filterStatus] = filterState; @@ -76,6 +140,8 @@ function Dashboard(props: DispatchProps & StateProps) { {role !== Role.Student && ( )} @@ -100,7 +166,7 @@ function Dashboard(props: DispatchProps & StateProps) {
    • - {generateAchievementTasks(inferencer.listSortedTaskUuids(), filterStatus, focusState)} + {generateAchievementTasks(inferencer.listSortedReleasedTaskUuids(), filterStatus, focusState)}
    diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 467841e3d2..ca6da2ce7f 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -2,12 +2,14 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; +import { fetchAssessmentOverviews } from '../../../commons/application/actions/SessionActions'; import { OverallState } from '../../../commons/application/ApplicationTypes'; import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import Constants from '../../../commons/utils/Constants'; import { getAchievements, getOwnGoals, + getUsers, updateGoalProgress } from '../../../features/achievement/AchievementActions'; import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; @@ -18,14 +20,18 @@ const mapStateToProps: MapStateToProps = state => ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) : new AchievementInferencer(mockAchievements, mockGoals), name: state.session.name, - role: state.session.role + role: state.session.role, + assessmentOverviews: state.session.assessmentOverviews, + users: state.achievement.users }); const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { + fetchAssessmentOverviews, getAchievements, getOwnGoals, + getUsers, updateGoalProgress }, dispatch diff --git a/src/styles/_achievementcontrol.scss b/src/styles/_achievementcontrol.scss index 4ff3b19084..d7870cd429 100644 --- a/src/styles/_achievementcontrol.scss +++ b/src/styles/_achievementcontrol.scss @@ -3,7 +3,7 @@ $cover-height: 18em; $cover-width: 36em; // Card aspect ratio 2:1 - $card-height: 5em; + $card-height: 7em; $card-width: 30em; // View aspect ratio 18:25 $view-height: 50em; diff --git a/src/styles/_achievementdashboard.scss b/src/styles/_achievementdashboard.scss index b25d44c497..f65060127c 100644 --- a/src/styles/_achievementdashboard.scss +++ b/src/styles/_achievementdashboard.scss @@ -37,7 +37,6 @@ p { font-size: 0.9em; margin: 0; - text-decoration: underline; } } From 20cc20fc817c59fc5db84796e8820ac8add9bffe Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 11:12:10 +0800 Subject: [PATCH 108/143] Fixed achievement tests, and a few other bug fixes --- .../utils/AchievementInferencer.ts | 19 +++-- src/commons/achievement/utils/DateHelper.ts | 2 + .../__tests__/AchievementInferencer.test.ts | 83 ++++++++++++------- src/commons/achievement/utils/eventHandler.ts | 4 +- src/commons/sagas/RequestsSaga.ts | 6 +- .../subcomponents/AchievementDashboard.tsx | 4 + 6 files changed, 81 insertions(+), 37 deletions(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 448dfc84d0..338bea6662 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -11,7 +11,7 @@ import { GoalDefinition, GoalType } from '../../../features/achievement/AchievementTypes'; -import { isExpired } from './DateHelper'; +import { isExpired, isReleased } from './DateHelper'; /** * An AchievementNode item encapsulates all important information of an achievement item @@ -248,11 +248,16 @@ class AchievementInferencer { */ public insertAchievement(achievement: AchievementItem) { // first, generate a new unique uuid - const newUuid = v4(); + let newUuid = v4(); + // a small overhead to truly guarantee uniqueness + while (this.nodeList.has(newUuid)) { + newUuid = v4(); + } // then assign the new unique uuid by overwriting the achievement item supplied by param // and insert it into nodeList achievement.uuid = newUuid; + this.nodeList.set(newUuid, new AchievementNode(achievement)); // finally, process the nodeList @@ -288,7 +293,11 @@ class AchievementInferencer { */ public insertGoalDefinition(definition: GoalDefinition) { // first, generate a new unique uuid by finding the max uuid - const newUuid = v4(); + let newUuid = v4(); + // a small overhead to truly guarantee uniqueness + while (this.goalList.has(newUuid)) { + newUuid = v4(); + } // then assign the new unique uuid by overwriting the goal item supplied by param // and insert it into goalList @@ -787,12 +796,12 @@ class AchievementInferencer { .map(goalUuid => this.getGoal(goalUuid).completed) .reduce((result, goalCompleted) => result && goalCompleted, true); - const isReleased = node.achievement.release === undefined || isExpired(node.achievement.release); + const hasReleased = isReleased(node.achievement.release); const hasUnexpiredDeadline = !isExpired(node.displayDeadline); node.status = achievementCompleted ? AchievementStatus.COMPLETED - : isReleased + : hasReleased ? hasUnexpiredDeadline ? AchievementStatus.ACTIVE : AchievementStatus.EXPIRED diff --git a/src/commons/achievement/utils/DateHelper.ts b/src/commons/achievement/utils/DateHelper.ts index 43db662001..5709a29d9c 100644 --- a/src/commons/achievement/utils/DateHelper.ts +++ b/src/commons/achievement/utils/DateHelper.ts @@ -2,6 +2,8 @@ const now = new Date(); export const isExpired = (deadline?: Date) => deadline !== undefined && deadline <= now; +export const isReleased = (release?: Date) => release === undefined || release <= now; + export const timeFromExpired = (deadline?: Date) => deadline === undefined ? 0 : deadline.getTime() - now.getTime(); diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 62eb4d93d7..a58ce40223 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -16,7 +16,7 @@ const testAchievement: AchievementItem = { uuid: '0', title: 'Test Achievement', ability: AchievementAbility.CORE, - xp: 0, + xp: 100, isTask: false, prerequisiteUuids: [], goalUuids: [], @@ -37,13 +37,26 @@ const testGoal: AchievementGoal = { achievementUuids: [], meta: { type: GoalType.MANUAL, - targetCount: 0 + targetCount: 1 }, count: 0, - targetCount: 0, + targetCount: 1, completed: false }; +const testGoalComplete: AchievementGoal = { + uuid: '0', + text: 'Test Goal', + achievementUuids: [], + meta: { + type: GoalType.MANUAL, + targetCount: 1 + }, + count: 1, + targetCount: 1, + completed: true +}; + describe('Achievement Inferencer Constructor', () => { test('Empty achievements and goals', () => { const inferencer = new AchievementInferencer([], []); @@ -88,8 +101,8 @@ describe('Achievement Inferencer Constructor', () => { ); test('Overwrites items of same IDs', () => { - expect(inferencer.getAllAchievements()).toEqual([testAchievement1, testAchievement2]); - expect(inferencer.getAllGoals()).toEqual([testGoal1, testGoal3]); + expect(inferencer.getAllAchievements()).toEqual([testAchievement1, testAchievement3]); + expect(inferencer.getAllGoals()).toEqual([testGoal2, testGoal3]); }); test('References the correct achievements and goals', () => { @@ -269,23 +282,22 @@ describe('Achievement Setter', () => { describe('Achievement Inferencer Getter', () => { // sorting based tests do not work with uuid implementation - /* const inferencer = new AchievementInferencer(mockAchievements, mockGoals); test('Get all achievement IDs', () => { - const achievementUuids = ['0', '1', '2', '3', '4', '5', '6', '8', '9', '13', '16', '21']; + const achievementUuids = ['0', '1', '13', '16', '2', '21', '3', '4', '5', '6', '8', '9']; expect(inferencer.getAllAchievementUuids().sort()).toEqual(achievementUuids); }); test('Get all goal IDs', () => { - const goalUuids = ['0', '1', '2', '3', '4', '5', '8', '11', '14', '16', '18']; + const goalUuids = ['0', '1', '11', '14', '16', '18', '2', '3', '4', '5', '8']; expect(inferencer.getAllGoalUuids().sort()).toEqual(goalUuids); }); test('List task IDs', () => { - const taskUuids = ['0', '4', '8', '13', '16', '21']; + const taskUuids = ['0', '13', '16', '21', '4', '8']; expect(inferencer.listTaskUuids().sort()).toEqual(taskUuids); }); @@ -295,7 +307,6 @@ describe('Achievement Inferencer Getter', () => { expect(inferencer.listSortedTaskUuids()).toEqual(sortedTaskUuids); }); - */ test('List goals', () => { const testAchievement1: AchievementItem = { @@ -465,47 +476,42 @@ describe('Achievement Prerequisite System', () => { }); }); -/* To be restored when XP system is fixed describe('Achievement XP System', () => { const testAchievement1: AchievementItem = { ...testAchievement, uuid: '1', - goalUuids: ['1', '2'] + goalUuids: ['1', '3'] }; const testAchievement2: AchievementItem = { ...testAchievement, uuid: '2', goalUuids: [] }; + const testAchievement3: AchievementItem = { ...testAchievement, uuid: '3', goalUuids: ['3'] }; - const testGoal1: AchievementGoal = { ...testGoal, uuid: '1', xp: 100, maxXp: 100 }; - const testGoal2: AchievementGoal = { ...testGoal, uuid: '2', xp: 20, maxXp: 100 }; - const testGoal3: AchievementGoal = { ...testGoal, uuid: '3', xp: 3, maxXp: 100 }; + const testGoal1: AchievementGoal = { ...testGoal, uuid: '1' }; + const testGoal2: AchievementGoal = { ...testGoal, uuid: '2' }; + const testGoal3: AchievementGoal = { ...testGoalComplete, uuid: '3' }; const inferencer = new AchievementInferencer( - [testAchievement1, testAchievement2], + [testAchievement1, testAchievement2, testAchievement3], [testGoal1, testGoal2, testGoal3] ); test('XP earned from an achievement', () => { - expect(inferencer.getAchievementXp('1')).toBe(120); - expect(inferencer.getAchievementXp('2')).toBe(0); + expect(inferencer.getAchievementXp('1')).toBe(100); + expect(inferencer.getAchievementXp('2')).toBe(100); + expect(inferencer.getAchievementXp('3')).toBe(100); expect(inferencer.getAchievementXp('101')).toBe(0); }); - test('Max XP earned from an achievement', () => { - expect(inferencer.getAchievementMaxXp('1')).toBe(200); - expect(inferencer.getAchievementMaxXp('2')).toBe(0); - expect(inferencer.getAchievementMaxXp('101')).toBe(0); - }); - - test('Total XP earned from all goals', () => { - expect(inferencer.getTotalXp()).toBe(123); + test('Total XP earned from all achievements', () => { + expect(inferencer.getTotalXp()).toBe(100); }); test('Progress frac from an achievement', () => { - expect(inferencer.getProgressFrac('1')).toBeCloseTo(120 / 200); + expect(inferencer.getProgressFrac('1')).toBe(1 / 2); expect(inferencer.getProgressFrac('2')).toBe(0); + expect(inferencer.getProgressFrac('3')).toBe(1); expect(inferencer.getProgressFrac('101')).toBe(0); }); }); -*/ describe('Achievement Display Deadline', () => { const expiredDeadline = new Date(1920, 1, 1); @@ -647,6 +653,27 @@ describe('Achievement Status', () => { expect(inferencer.getStatus('2')).toBe(AchievementStatus.EXPIRED); }); + + test('Unreleased status', () => { + const expiredDeadline = new Date(1920, 1, 1); + const unexpiredDeadline = new Date(2220, 1, 1); + + const unreleased: AchievementItem = { ...partiallyCompleted, release: unexpiredDeadline }; + const released: AchievementItem = { ...notCompleted, release: expiredDeadline }; + const precompleted: AchievementItem = { ...fullyCompleted, release: unexpiredDeadline }; + + const inferencer = new AchievementInferencer( + [unreleased, released, precompleted], + [testGoal1, testGoal2, testGoal3] + ); + + expect(inferencer.getStatus('1')).not.toBe(AchievementStatus.UNRELEASED); + expect(inferencer.getStatus('2')).toBe(AchievementStatus.UNRELEASED); + expect(inferencer.getStatus('3')).not.toBe(AchievementStatus.UNRELEASED); + + expect(inferencer.getStatus('1')).toBe(AchievementStatus.COMPLETED); + expect(inferencer.getStatus('3')).toBe(AchievementStatus.ACTIVE); + }); }); describe('Achievement Position', () => { diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 37a0e1586d..13b817038f 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -51,8 +51,8 @@ function eventConditionSatisfied(meta: EventMeta): boolean { } let inferencer: AchievementInferencer = new AchievementInferencer( - store.getState().achievement.achievements, - store.getState().achievement.goals + store ? store.getState().achievement.achievements : [], + store ? store.getState().achievement.goals : [] ); const goalIncludesEvent = (goal: AchievementGoal, eventName: EventType) => { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 7c0dc73c25..90677c0eeb 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -137,6 +137,8 @@ export const getAchievements = async (tokens: Tokens): Promise ({ @@ -144,8 +146,8 @@ export const getAchievements = async (tokens: Tokens): Promise { + // No goals for contests and practical assessments that don't give XP + if (assessmentOverview.category === 'Contest' || assessmentOverview.category === 'Practical') { + return; + } const idString = assessmentOverview.id.toString(); if (!inferencer.hasAchievement(idString)) { // Goal for assessment submission From dfdaf0fe84f5ccdd0b4916b8d3146e25a54637be Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 11:15:36 +0800 Subject: [PATCH 109/143] Fixed formatting --- .../achievement/AchievementManualEditor.tsx | 17 ++- .../achievement/control/GoalEditor.tsx | 4 +- .../achievementEditor/EditableCard.tsx | 17 ++- .../achievement/overview/AchievementLevel.tsx | 2 +- .../utils/AchievementInferencer.ts | 59 ++++++---- src/commons/achievement/utils/eventHandler.ts | 4 +- .../achievement/view/AchievementViewGoal.tsx | 4 +- src/commons/sagas/RequestsSaga.ts | 8 +- .../achievement/AchievementReducer.ts | 2 +- src/features/achievement/AchievementTypes.ts | 2 +- .../subcomponents/AchievementDashboard.tsx | 103 ++++++++++++------ 11 files changed, 140 insertions(+), 82 deletions(-) diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index 05b7571814..9278c7fa00 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -22,10 +22,13 @@ const goalRenderer: ItemRenderer = (goal, { handleClick }) => ( function AchievementManualEditor(props: AchievementManualEditorProps) { const { studio, getUsers, updateGoalProgress } = props; - const users = studio === 'Staff' - ? props.users.sort((user1, user2) => user1.name.localeCompare(user2.name)) - // Not sure how studio is represented as a string - : props.users.filter(user => user.group === studio).sort((user1, user2) => user1.name.localeCompare(user2.name)); + const users = + studio === 'Staff' + ? props.users.sort((user1, user2) => user1.name.localeCompare(user2.name)) + : // Not sure how studio is represented as a string + props.users + .filter(user => user.group === studio) + .sort((user1, user2) => user1.name.localeCompare(user2.name)); useEffect(() => getUsers(), [getUsers]); @@ -74,7 +77,11 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { itemRenderer={userRenderer} onItemSelect={changeSelectedUser} > -
    ); } diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 41ca3770bb..675edfde3a 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -174,17 +174,16 @@ function EditableCard(props: EditableCardProps) { dispatch({ type: ActionType.CHANGE_GOAL_UUIDS, payload: goalUuids }); // add the current achievement into the goals chosen goalUuids.forEach(goalUuid => { - const goal = inferencer.getGoal(goalUuid); - // iterate through the achievements, if the uuid is ot found, add it in - const len = goal.achievementUuids.length; - for (let i = 0; i < len; i++) { - if (goal.achievementUuids[i] === uuid) { - return; - } + const goal = inferencer.getGoal(goalUuid); + // iterate through the achievements, if the uuid is ot found, add it in + const len = goal.achievementUuids.length; + for (let i = 0; i < len; i++) { + if (goal.achievementUuids[i] === uuid) { + return; } - goal.achievementUuids[len] = uuid; } - ); + goal.achievementUuids[len] = uuid; + }); }; const changePosition = (position: number) => diff --git a/src/commons/achievement/overview/AchievementLevel.tsx b/src/commons/achievement/overview/AchievementLevel.tsx index f8cdc20b45..164b721681 100644 --- a/src/commons/achievement/overview/AchievementLevel.tsx +++ b/src/commons/achievement/overview/AchievementLevel.tsx @@ -36,7 +36,7 @@ function AchievementLevel(props: AchievementLevelProps) { {progress} / {xpPerLevel} XP

    - {showMilestone && } + {showMilestone && }
    ); } diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 338bea6662..4cc8316dd0 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -73,11 +73,11 @@ class AchievementInferencer { uuid: 'invalid', text: 'invalid', achievementUuids: [], - meta: {type: GoalType.MANUAL, targetCount: 0}, + meta: { type: GoalType.MANUAL, targetCount: 0 }, count: 0, targetCount: 0, completed: false - } + }; /** * Invalid Achievement for the getters to return if the goal does not exist in the goalList @@ -94,8 +94,8 @@ class AchievementInferencer { prerequisiteUuids: [], goalUuids: [], cardBackground: 'invalid', - view: {coverImage: 'invalid', description: 'invalid', completionText: 'invalid'} - } + view: { coverImage: 'invalid', description: 'invalid', completionText: 'invalid' } + }; /** * Returns an array of AchievementItem @@ -114,7 +114,9 @@ class AchievementInferencer { } public getAllCompletedAchievements() { - return [...this.nodeList.values()].filter(node => node.status === AchievementStatus.COMPLETED).map(node => node.achievement); + return [...this.nodeList.values()] + .filter(node => node.status === AchievementStatus.COMPLETED) + .map(node => node.achievement); } /** @@ -215,7 +217,7 @@ class AchievementInferencer { /** * Returns an array of achievements that use the goal - * + * * @param goalUuid UUID of the goal in question */ public getAchievementsByGoal(goalUuid: string) { @@ -224,7 +226,7 @@ class AchievementInferencer { /** * Returns true if the goal is invalid - * + * * @param goal An AchievementGoal */ public isInvalidGoal(goal: AchievementGoal) { @@ -233,10 +235,10 @@ class AchievementInferencer { /** * Returns true if the achievement is invalid - * + * * @param achievement An AchievementItem */ - public isInvalidAchievement (achievement: AchievementItem) { + public isInvalidAchievement(achievement: AchievementItem) { return achievement === this.invalidAchievement; } @@ -310,7 +312,7 @@ class AchievementInferencer { return newUuid; } - /** + /** * Inserts a new GoalDefinition into the Inferencer, * then returns the newly assigned goalUuid * Used for inserting assessment goals on the fly @@ -322,15 +324,25 @@ class AchievementInferencer { // then assign the new unique uuid by overwriting the goal item supplied by param // and insert it into goalList if (complete) { - this.goalList.set(definition.uuid, { ...definition, count: 1, targetCount: 1, completed: true}); + this.goalList.set(definition.uuid, { + ...definition, + count: 1, + targetCount: 1, + completed: true + }); } else { - this.goalList.set(definition.uuid, { ...definition, count: 0, targetCount: 1, completed: false}); + this.goalList.set(definition.uuid, { + ...definition, + count: 0, + targetCount: 1, + completed: false + }); } // finally, process the goalList this.processGoals(); - return definition.uuid + return definition.uuid; } /** @@ -438,11 +450,10 @@ class AchievementInferencer { .map(task => task.uuid); } - /** * Returns an array of achievementId that isTask or is completed sorted by position */ - public listSortedTaskUuids() { + public listSortedTaskUuids() { return this.getAllAchievements() .filter(achievement => achievement.isTask || this.isCompleted(achievement)) .sort((taskA, taskB) => taskA.position - taskB.position) @@ -553,7 +564,10 @@ class AchievementInferencer { * Returns total XP earned from all achievements */ public getTotalXp() { - return this.getAllCompletedAchievements().reduce((totalXp, achievement) => totalXp + achievement.xp, 0); + return this.getAllCompletedAchievements().reduce( + (totalXp, achievement) => totalXp + achievement.xp, + 0 + ); } /** @@ -773,7 +787,10 @@ class AchievementInferencer { node.progressFrac = 0; } else { const num = goalUuids.reduce((count, goalUuid) => count + this.getGoal(goalUuid).count, 0); - const denom = goalUuids.reduce((count, goalUuid) => count + this.getGoal(goalUuid).targetCount, 0); + const denom = goalUuids.reduce( + (count, goalUuid) => count + this.getGoal(goalUuid).targetCount, + 0 + ); node.progressFrac = Math.min(denom === 0 ? 0 : num / denom, 1); } } @@ -802,10 +819,10 @@ class AchievementInferencer { node.status = achievementCompleted ? AchievementStatus.COMPLETED : hasReleased - ? hasUnexpiredDeadline - ? AchievementStatus.ACTIVE - : AchievementStatus.EXPIRED - : AchievementStatus.UNRELEASED; + ? hasUnexpiredDeadline + ? AchievementStatus.ACTIVE + : AchievementStatus.EXPIRED + : AchievementStatus.UNRELEASED; } /** diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 13b817038f..12758bd5fd 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -133,7 +133,7 @@ export function processEvent(eventName: EventType, increment: number = 1) { ); processEvent(eventName, increment); }; - + if (!store.getState().achievement.goals[0]) { // ensure that the next function call has updated XP values store.dispatch(getOwnGoals()); @@ -149,4 +149,4 @@ export function processEvent(eventName: EventType, increment: number = 1) { retry(); } } -} \ No newline at end of file +} diff --git a/src/commons/achievement/view/AchievementViewGoal.tsx b/src/commons/achievement/view/AchievementViewGoal.tsx index da8d4d9c56..dedf77a58c 100644 --- a/src/commons/achievement/view/AchievementViewGoal.tsx +++ b/src/commons/achievement/view/AchievementViewGoal.tsx @@ -23,9 +23,7 @@ const mapGoalToJSX = (goal: AchievementGoal) => {

    -

    - {text} -

    +

    {text}

    => { +export const getAllUsers = async (tokens: Tokens): Promise => { const resp = await request('admin/users', 'GET', { ...tokens, shouldRefresh: true diff --git a/src/features/achievement/AchievementReducer.ts b/src/features/achievement/AchievementReducer.ts index 33d4faaea3..0c29714099 100644 --- a/src/features/achievement/AchievementReducer.ts +++ b/src/features/achievement/AchievementReducer.ts @@ -23,7 +23,7 @@ export const AchievementReducer: Reducer = ( return { ...state, users: action.payload - } + }; default: return state; } diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index f677d54740..bdfa532203 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -173,7 +173,7 @@ export type AchievementUser = { name: string; userId: number; group: string; -} +}; export type AchievementState = { achievements: AchievementItem[]; diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 112fd60d32..44d21cf859 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -10,8 +10,18 @@ import AchievementTask from '../../../commons/achievement/AchievementTask'; import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import Constants from '../../../commons/utils/Constants'; -import { AchievementContext, cardBackgroundUrl, coverImageUrl } from '../../../features/achievement/AchievementConstants'; -import { AchievementAbility, AchievementUser, FilterStatus, GoalProgress, GoalType } from '../../../features/achievement/AchievementTypes'; +import { + AchievementContext, + cardBackgroundUrl, + coverImageUrl +} from '../../../features/achievement/AchievementConstants'; +import { + AchievementAbility, + AchievementUser, + FilterStatus, + GoalProgress, + GoalType +} from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { fetchAssessmentOverviews: () => void; @@ -52,8 +62,19 @@ export const generateAchievementTasks = ( )); function Dashboard(props: DispatchProps & StateProps) { - const { getAchievements, getOwnGoals, getUsers, updateGoalProgress, fetchAssessmentOverviews, - group, inferencer, name, role, assessmentOverviews, users } = props; + const { + getAchievements, + getOwnGoals, + getUsers, + updateGoalProgress, + fetchAssessmentOverviews, + group, + inferencer, + name, + role, + assessmentOverviews, + users + } = props; /** * Fetch the latest achievements and goals from backend when the page is rendered @@ -80,50 +101,58 @@ function Dashboard(props: DispatchProps & StateProps) { if (!inferencer.hasAchievement(idString)) { // Goal for assessment submission inferencer.insertFakeGoalDefinition( - { uuid: idString + '0', + { + uuid: idString + '0', text: `Submitted ${assessmentOverview.category.toLowerCase()}`, achievementUuids: [idString], - meta: { - type: GoalType.ASSESSMENT, - assessmentNumber: assessmentOverview.id, + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, requiredCompletionFrac: 0 } - }, assessmentOverview.status === 'submitted' - ) + }, + assessmentOverview.status === 'submitted' + ); // Goal for assessment grading inferencer.insertFakeGoalDefinition( - { uuid: idString + '1', + { + uuid: idString + '1', text: `Graded ${assessmentOverview.category.toLowerCase()}`, achievementUuids: [idString], - meta: { - type: GoalType.ASSESSMENT, - assessmentNumber: assessmentOverview.id, + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, requiredCompletionFrac: 0 } - }, assessmentOverview.gradingStatus === 'graded' - ) + }, + assessmentOverview.gradingStatus === 'graded' + ); // Would like a goal for early submission, but that seems to be hard to get from the overview - inferencer.insertFakeAchievement( - { uuid: idString, - title: assessmentOverview.title, - ability: assessmentOverview.category === 'Mission' || assessmentOverview.category === 'Path' + inferencer.insertFakeAchievement({ + uuid: idString, + title: assessmentOverview.title, + ability: + assessmentOverview.category === 'Mission' || assessmentOverview.category === 'Path' ? AchievementAbility.CORE : AchievementAbility.EFFORT, - xp: assessmentOverview.gradingStatus === 'graded' ? assessmentOverview.xp : assessmentOverview.maxXp, - deadline: new Date(assessmentOverview.closeAt), - release: new Date(assessmentOverview.openAt), - isTask: assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, - position: -1, // always appears on top - prerequisiteUuids: [], - goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete - cardBackground: `${cardBackgroundUrl}/default.png`, - view: { - coverImage: `${coverImageUrl}/default.png`, - description: assessmentOverview.shortSummary, - completionText: `Grade: ${assessmentOverview.grade} / ${assessmentOverview.maxGrade}` - } + xp: + assessmentOverview.gradingStatus === 'graded' + ? assessmentOverview.xp + : assessmentOverview.maxXp, + deadline: new Date(assessmentOverview.closeAt), + release: new Date(assessmentOverview.openAt), + isTask: + assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, + position: -1, // always appears on top + prerequisiteUuids: [], + goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete + cardBackground: `${cardBackgroundUrl}/default.png`, + view: { + coverImage: `${coverImageUrl}/default.png`, + description: assessmentOverview.shortSummary, + completionText: `Grade: ${assessmentOverview.grade} / ${assessmentOverview.maxGrade}` } - ) + }); } }); @@ -170,7 +199,11 @@ function Dashboard(props: DispatchProps & StateProps) {
      - {generateAchievementTasks(inferencer.listSortedReleasedTaskUuids(), filterStatus, focusState)} + {generateAchievementTasks( + inferencer.listSortedReleasedTaskUuids(), + filterStatus, + focusState + )}
    From d7cc61bfab7f43c134734c9a29eb48faeaa78682 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 12:01:07 +0800 Subject: [PATCH 110/143] Fixed tests, handled null deadline. --- .../utils/__tests__/AchievementInferencer.test.ts | 12 ++++++------ src/commons/sagas/RequestsSaga.ts | 8 ++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index a58ce40223..264d414d7a 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -240,7 +240,7 @@ describe('Achievement Setter', () => { false ) ).toBeFalsy(); - expect(inferencer.getTitleByUuid('2')).toBeUndefined(); + expect(inferencer.getTitleByUuid('2')).toBe('invalid'); expect(inferencer.getUuidByTitle(title)).toBeUndefined(); }); @@ -274,7 +274,7 @@ describe('Achievement Setter', () => { false ) ).toBeFalsy(); - expect(inferencer.getTextByUuid('2')).toBeUndefined(); + expect(inferencer.getTextByUuid('2')).toBe('invalid'); expect(inferencer.getUuidByText(text)).toBeUndefined(); }); }); @@ -297,13 +297,13 @@ describe('Achievement Inferencer Getter', () => { }); test('List task IDs', () => { - const taskUuids = ['0', '13', '16', '21', '4', '8']; + const taskUuids = ['0', '1', '13', '16', '21', '4', '5', '8']; expect(inferencer.listTaskUuids().sort()).toEqual(taskUuids); }); test('List sorted task IDs', () => { - const sortedTaskUuids = ['0', '8', '21', '4', '16', '13']; + const sortedTaskUuids = ['1', '5', '0', '8', '21', '4', '16', '13']; expect(inferencer.listSortedTaskUuids()).toEqual(sortedTaskUuids); }); @@ -368,7 +368,7 @@ describe('Achievement ID to Title', () => { const inferencer = new AchievementInferencer([testAchievement1], []); test('Non-existing achievement ID', () => { - expect(inferencer.getTitleByUuid('1')).toBeUndefined(); + expect(inferencer.getTitleByUuid('1')).toBe('invalid'); }); test('Existing achievement ID', () => { @@ -392,7 +392,7 @@ describe('Goal ID to Text', () => { const inferencer = new AchievementInferencer([], [testGoal1]); test('Non-existing goal ID', () => { - expect(inferencer.getTextByUuid('1')).toBeUndefined(); + expect(inferencer.getTextByUuid('1')).toBe('invalid'); }); test('Existing goal ID', () => { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 8e0b3f5e60..c862e18482 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -137,8 +137,6 @@ export const getAchievements = async (tokens: Tokens): Promise ({ @@ -146,10 +144,8 @@ export const getAchievements = async (tokens: Tokens): Promise Date: Thu, 15 Apr 2021 13:33:09 +0800 Subject: [PATCH 111/143] Moved Run Code event to when button is clicked. Run Code event moved out of WorkspaceSaga to ControlBarRunButton and MobileSideContent. --- src/commons/controlBar/ControlBarRunButton.tsx | 10 +++++++++- .../mobileSideContent/MobileSideContent.tsx | 4 ++++ src/commons/sagas/WorkspaceSaga.ts | 5 ----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index b624f4163b..8a56eda2e5 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -2,6 +2,8 @@ import { Position } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; +import { EventType } from '../../features/achievement/AchievementTypes'; +import { processEvent } from '../achievement/utils/eventHandler'; import controlButton from '../ControlButton'; type ControlButtonRunButtonProps = DispatchProps & StateProps; @@ -17,7 +19,13 @@ type StateProps = { export function ControlBarRunButton(props: ControlButtonRunButtonProps) { return ( - {controlButton('Run', IconNames.PLAY, props.handleEditorEval)} + {controlButton( + 'Run', + IconNames.PLAY, + () => { + props.handleEditorEval(); + processEvent(EventType.RUNCODE); + })} ); } diff --git a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx index 8f0329cc69..e5c76456e6 100644 --- a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx +++ b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx @@ -3,6 +3,8 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import React from 'react'; import { useSelector } from 'react-redux'; +import { EventType } from '../../../features/achievement/AchievementTypes'; +import { processEvent } from '../../achievement/utils/eventHandler'; import { OverallState } from '../../application/ApplicationTypes'; import { ControlBarProps } from '../../controlBar/ControlBar'; import { SideContentTab, SideContentType } from '../../sideContent/SideContentTypes'; @@ -195,8 +197,10 @@ const MobileSideContent: React.FC = props => newTabId === SideContentType.mobileEditorRun ) { props.handleEditorEval(); + processEvent(EventType.RUNCODE); } else if (newTabId === SideContentType.mobileEditorRun) { props.handleEditorEval(); + processEvent(EventType.RUNCODE); props.handleShowRepl(); } else { props.handleHideRepl(); diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index ca6046c358..e5f10e9b55 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -22,10 +22,8 @@ import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select, take } from 'redux-saga/effects'; import * as Sourceror from 'sourceror'; -import { EventType } from '../../features/achievement/AchievementTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { DeviceSession } from '../../features/remoteExecution/RemoteExecutionTypes'; -import { processEvent } from '../achievement/utils/eventHandler'; import { OverallState, styliseSublanguage } from '../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../application/types/ExternalTypes'; import { @@ -118,9 +116,6 @@ export default function* WorkspaceSaga(): SagaIterator { globals }; - // achievement: runCode - processEvent(EventType.RUNCODE); - if (remoteExecutionSession && remoteExecutionSession.workspace === workspaceLocation) { yield put(actions.remoteExecRun(editorCode)); } else { From 58189bafb3c0346062717b4c41942aa7363fb626 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 14:06:15 +0800 Subject: [PATCH 112/143] Fixed formatting. --- src/commons/controlBar/ControlBarRunButton.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index 8a56eda2e5..4e404b4f02 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -19,13 +19,10 @@ type StateProps = { export function ControlBarRunButton(props: ControlButtonRunButtonProps) { return ( - {controlButton( - 'Run', - IconNames.PLAY, - () => { - props.handleEditorEval(); - processEvent(EventType.RUNCODE); - })} + {controlButton('Run', IconNames.PLAY, () => { + props.handleEditorEval(); + processEvent(EventType.RUNCODE); + })} ); } From 76dd01ae5ccda674503004272baef06486dce588 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 18:03:35 +0800 Subject: [PATCH 113/143] Replace deprecated Tooltip with Tooltip2. --- .../achievementEditor/AchievementSettings.tsx | 7 ++--- .../achievementEditor/EditableAbility.tsx | 7 ++--- .../achievementEditor/EditableCard.tsx | 19 ++++++++----- .../achievementEditor/EditableDate.tsx | 7 ++--- .../achievementEditor/EditableView.tsx | 7 ++--- .../control/common/ItemDeleter.tsx | 7 ++--- .../achievement/control/common/ItemSaver.tsx | 11 ++++---- .../control/goalEditor/EditableMeta.tsx | 7 ++--- .../metaDetails/EditableAssessmentMeta.tsx | 11 ++++---- .../metaDetails/EditableBinaryMeta.tsx | 19 ++++++------- .../metaDetails/EditableEventMeta.tsx | 27 ++++++++++--------- .../metaDetails/EditableManualMeta.tsx | 7 ++--- 12 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index 256bec6206..90bf19f944 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -1,5 +1,6 @@ -import { Button, Dialog, EditableText, Tooltip } from '@blueprintjs/core'; +import { Button, Dialog, EditableText } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { Tooltip2 } from '@blueprintjs/popover2'; import React, { useState } from 'react'; import { AchievementItem } from 'src/features/achievement/AchievementTypes'; @@ -30,9 +31,9 @@ function AchievementSettings(props: AchievementSettingsProps) { return ( <> - + - + - +
    @@ -149,7 +150,7 @@ function EditableBinaryMeta(props: EditableBinaryMetaProps) { return ( <> - + - + {generateConditions()}
    + + + + + + ); +} + +export default EditableDate; diff --git a/src/commons/achievement/control/goalEditor/EditableTime.tsx b/src/commons/achievement/control/goalEditor/EditableTime.tsx new file mode 100644 index 0000000000..382df72150 --- /dev/null +++ b/src/commons/achievement/control/goalEditor/EditableTime.tsx @@ -0,0 +1,45 @@ +import { Button, Dialog } from '@blueprintjs/core'; +import { TimePicker } from '@blueprintjs/datetime'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import { useState } from 'react'; +import { prettifyTime } from 'src/commons/achievement/utils/DateHelper'; + +type EditableTimeProps = { + type: string; + time?: Date; + changeTime: (time?: Date) => void; +}; + +function EditableTime(props: EditableTimeProps) { + const { type, time, changeTime } = props; + + const [isOpen, setOpen] = useState(false); + const toggleOpen = () => setOpen(!isOpen); + + const hoverText = time === undefined ? `No ${type}` : `${prettifyTime(time)}`; + + return ( + <> + + + + + + + + ); +} + +export default EditableTime; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 4cc8316dd0..28403537e0 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -805,18 +805,18 @@ class AchievementInferencer { * @param node the AchievementNode */ private generateStatus(node: AchievementNode) { - const { goalUuids } = node.achievement; + let prereqsCompleted = true; + // iterate through all prerequisites. If any are not complete, then prereqs not complete. + node.descendant.forEach(uuid => { + prereqsCompleted = prereqsCompleted && this.isCompleted(this.getAchievement(uuid)); + }); - const achievementCompleted = - goalUuids.length !== 0 && - goalUuids - .map(goalUuid => this.getGoal(goalUuid).completed) - .reduce((result, goalCompleted) => result && goalCompleted, true); + const achievementCompleted = this.isCompleted(node.achievement); const hasReleased = isReleased(node.achievement.release); const hasUnexpiredDeadline = !isExpired(node.displayDeadline); - node.status = achievementCompleted + node.status = achievementCompleted && prereqsCompleted ? AchievementStatus.COMPLETED : hasReleased ? hasUnexpiredDeadline From 4f50f8a233def16cdc2526180e052cb80c3be9ae Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 15 Apr 2021 21:45:55 +0800 Subject: [PATCH 116/143] Fixed formatting. --- .../achievement/utils/AchievementBackender.ts | 74 ++++++++++--------- .../utils/AchievementInferencer.ts | 15 ++-- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 10bcd7c57a..8e769c24a4 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -2,8 +2,8 @@ import { AchievementAbility, AchievementGoal, AchievementItem, - GoalDefinition, - GoalMeta, + GoalDefinition, + GoalMeta, GoalType } from '../../../features/achievement/AchievementTypes'; @@ -16,37 +16,41 @@ export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ achievementUuids: goal.achievementUuids }); -export const frontendifyAchievementGoal = (goal: any) => ({ - uuid: goal.uuid || '', - text: goal.text || '', - achievementUuids: goal.achievementUuids, - meta: (goal.meta.type === 'Event' - ? { ...goal.meta, - release: goal.meta.release ? new Date(goal.meta.release) : undefined, - deadline: goal.meta.deadline ? new Date(goal.meta.deadline) : undefined, - observeFrom: goal.meta.observeFrom ? new Date(goal.meta.observeFrom) : undefined, - observeTo: goal.meta.observeTo ? new Date(goal.meta.observeTo) : undefined} - : goal.meta) as GoalMeta, - count: goal.count, - targetCount: goal.targetCount, - completed: goal.completed -} as AchievementGoal); +export const frontendifyAchievementGoal = (goal: any) => + ({ + uuid: goal.uuid || '', + text: goal.text || '', + achievementUuids: goal.achievementUuids, + meta: (goal.meta.type === 'Event' + ? { + ...goal.meta, + release: goal.meta.release ? new Date(goal.meta.release) : undefined, + deadline: goal.meta.deadline ? new Date(goal.meta.deadline) : undefined, + observeFrom: goal.meta.observeFrom ? new Date(goal.meta.observeFrom) : undefined, + observeTo: goal.meta.observeTo ? new Date(goal.meta.observeTo) : undefined + } + : goal.meta) as GoalMeta, + count: goal.count, + targetCount: goal.targetCount, + completed: goal.completed + } as AchievementGoal); -export const frontendifyAchievementItem = (achievement: any) => ({ - uuid: achievement.uuid || '', - title: achievement.title || '', - ability: achievement.ability as AchievementAbility, - xp: achievement.xp, - deadline: achievement.deadline === null ? undefined : new Date(achievement.deadline), - release: achievement.release === null ? undefined : new Date(achievement.release), - isTask: achievement.isTask, - position: achievement.position, - prerequisiteUuids: achievement.prerequisiteUuids, - goalUuids: achievement.goalUuids, - cardBackground: achievement.cardBackground || '', - view: { - coverImage: achievement.view.coverImage || '', - completionText: achievement.view.completionText || '', - description: achievement.view.description || '' - } -} as AchievementItem); +export const frontendifyAchievementItem = (achievement: any) => + ({ + uuid: achievement.uuid || '', + title: achievement.title || '', + ability: achievement.ability as AchievementAbility, + xp: achievement.xp, + deadline: achievement.deadline === null ? undefined : new Date(achievement.deadline), + release: achievement.release === null ? undefined : new Date(achievement.release), + isTask: achievement.isTask, + position: achievement.position, + prerequisiteUuids: achievement.prerequisiteUuids, + goalUuids: achievement.goalUuids, + cardBackground: achievement.cardBackground || '', + view: { + coverImage: achievement.view.coverImage || '', + completionText: achievement.view.completionText || '', + description: achievement.view.description || '' + } + } as AchievementItem); diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 28403537e0..17cb3e7d2e 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -816,13 +816,14 @@ class AchievementInferencer { const hasReleased = isReleased(node.achievement.release); const hasUnexpiredDeadline = !isExpired(node.displayDeadline); - node.status = achievementCompleted && prereqsCompleted - ? AchievementStatus.COMPLETED - : hasReleased - ? hasUnexpiredDeadline - ? AchievementStatus.ACTIVE - : AchievementStatus.EXPIRED - : AchievementStatus.UNRELEASED; + node.status = + achievementCompleted && prereqsCompleted + ? AchievementStatus.COMPLETED + : hasReleased + ? hasUnexpiredDeadline + ? AchievementStatus.ACTIVE + : AchievementStatus.EXPIRED + : AchievementStatus.UNRELEASED; } /** From bd937ce2ec074654160bcf034f59ada8df886c0a Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 15:47:09 +0800 Subject: [PATCH 117/143] Added multiple goal and event types. Error and runcode event, xp and achievements goals. --- .../achievement/AchievementManualEditor.tsx | 2 +- .../control/goalEditor/EditableMeta.tsx | 15 ++- .../control/goalEditor/GoalTemplate.ts | 12 +- .../metaDetails/EditableAchievementsMeta.tsx | 32 +++++ .../metaDetails/EditableEventMeta.tsx | 10 +- .../goalEditor/metaDetails/EditableXpMeta.tsx | 32 +++++ .../utils/AchievementInferencer.ts | 2 +- .../utils/InsertFakeAchievements.ts | 75 +++++++++++ src/commons/achievement/utils/eventHandler.ts | 27 ++-- .../controlBar/ControlBarRunButton.tsx | 2 +- .../mobileSideContent/MobileSideContent.tsx | 4 +- src/commons/sagas/WorkspaceSaga.ts | 11 +- src/commons/sideContent/SideContent.tsx | 4 - src/features/achievement/AchievementTypes.ts | 30 ++++- .../subcomponents/AchievementDashboard.tsx | 127 ++++++++---------- .../AchievementDashboardContainer.ts | 1 + 16 files changed, 284 insertions(+), 102 deletions(-) create mode 100644 src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx create mode 100644 src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx create mode 100644 src/commons/achievement/utils/InsertFakeAchievements.ts diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index 9278c7fa00..29426562af 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -70,7 +70,7 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { } else { return (
    -

    User ID:

    +

    User:

    void; @@ -45,6 +49,15 @@ function EditableMeta(props: EditableMetaProps) { return ; case GoalType.EVENT: return ; + case GoalType.XP: + return ; + case GoalType.ACHIEVEMENTS: + return ( + + ); default: return null; } diff --git a/src/commons/achievement/control/goalEditor/GoalTemplate.ts b/src/commons/achievement/control/goalEditor/GoalTemplate.ts index 50d52ab98d..9db4200fd3 100644 --- a/src/commons/achievement/control/goalEditor/GoalTemplate.ts +++ b/src/commons/achievement/control/goalEditor/GoalTemplate.ts @@ -29,13 +29,23 @@ export const metaTemplate = (type: GoalType): GoalMeta => { case GoalType.EVENT: return { type: GoalType.EVENT, - eventNames: [EventType.NONE], + eventNames: [EventType.RUN_CODE], targetCount: 1, release: undefined, deadline: undefined, observeFrom: undefined, observeTo: undefined }; + case GoalType.XP: + return { + type: GoalType.XP, + targetCount: 100 + }; + case GoalType.ACHIEVEMENTS: + return { + type: GoalType.ACHIEVEMENTS, + targetCount: 1 + }; } }; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx new file mode 100644 index 0000000000..5992bd89e0 --- /dev/null +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx @@ -0,0 +1,32 @@ +import { NumericInput } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import { AchievementsMeta, GoalMeta } from 'src/features/achievement/AchievementTypes'; + +type EditableAchievementsMetaProps = { + changeMeta: (meta: GoalMeta) => void; + achievementsMeta: AchievementsMeta; +}; + +function EditableAchievementsMeta(props: EditableAchievementsMetaProps) { + const { changeMeta, achievementsMeta } = props; + const { targetCount } = achievementsMeta; + + const changeTargetCount = (targetCount: number) => + changeMeta({ ...achievementsMeta, targetCount: targetCount }); + + return ( + + + + ); +} + +export default EditableAchievementsMeta; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx index ef6de92e8e..51cd58360f 100644 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableEventMeta.tsx @@ -25,8 +25,12 @@ function EditableEventMeta(props: EditableEventMetaProps) { changeMeta({ ...eventMeta, targetCount: targetCount }); const changeEventName = (eventName: EventType, index: number) => { - eventNames[index] = eventName; - changeMeta({ ...eventMeta, eventNames: eventNames }); + if (eventName === EventType.NONE) { + changeMeta({ ...eventMeta, eventNames: eventNames.filter((_, idx) => idx !== index) }); + } else { + eventNames[index] = eventName; + changeMeta({ ...eventMeta, eventNames: eventNames }); + } }; const changeRelease = (release?: Date) => { @@ -64,7 +68,7 @@ function EditableEventMeta(props: EditableEventMetaProps) { }; const addEvent = () => { - eventNames[eventNames.length] = EventType.NONE; + eventNames[eventNames.length] = EventType.RUN_CODE; changeMeta({ ...eventMeta, eventNames: eventNames }); }; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx new file mode 100644 index 0000000000..960bd52437 --- /dev/null +++ b/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx @@ -0,0 +1,32 @@ +import { NumericInput } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import { GoalMeta, XpMeta } from 'src/features/achievement/AchievementTypes'; + +type EditableXpMetaProps = { + changeMeta: (meta: GoalMeta) => void; + xpMeta: XpMeta; +}; + +function EditableXpMeta(props: EditableXpMetaProps) { + const { changeMeta, xpMeta } = props; + const { targetCount } = xpMeta; + + const changeTargetCount = (targetCount: number) => + changeMeta({ ...xpMeta, targetCount: targetCount }); + + return ( + + + + ); +} + +export default EditableXpMeta; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 17cb3e7d2e..7a4a0dd134 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -475,7 +475,7 @@ class AchievementInferencer { * * NOTE: It might be better (more efficient) to simply have a completed proporty on each achievement. */ - private isCompleted(achievement: AchievementItem) { + public isCompleted(achievement: AchievementItem) { const goalLength = achievement.goalUuids.length; // an achievement with no goals should not be considered complete if (goalLength === 0) { diff --git a/src/commons/achievement/utils/InsertFakeAchievements.ts b/src/commons/achievement/utils/InsertFakeAchievements.ts new file mode 100644 index 0000000000..5ab49745f5 --- /dev/null +++ b/src/commons/achievement/utils/InsertFakeAchievements.ts @@ -0,0 +1,75 @@ +import { + cardBackgroundUrl, + coverImageUrl +} from '../../../features/achievement/AchievementConstants'; +import { AchievementAbility, GoalType } from '../../../features/achievement/AchievementTypes'; +import { AssessmentOverview } from '../../assessment/AssessmentTypes'; +import AchievementInferencer from './AchievementInferencer'; + +function insertFakeAchievements( + assessmentOverview: AssessmentOverview, + inferencer: AchievementInferencer +) { + // No goals for contests and practical assessments that don't give XP + if (assessmentOverview.category === 'Contest' || assessmentOverview.category === 'Practical') { + return; + } + const idString = assessmentOverview.id.toString(); + if (!inferencer.hasAchievement(idString)) { + // Goal for assessment submission + inferencer.insertFakeGoalDefinition( + { + uuid: idString + '0', + text: `Submitted ${assessmentOverview.category.toLowerCase()}`, + achievementUuids: [idString], + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, + requiredCompletionFrac: 0 + } + }, + assessmentOverview.status === 'submitted' + ); + // Goal for assessment grading + inferencer.insertFakeGoalDefinition( + { + uuid: idString + '1', + text: `Graded ${assessmentOverview.category.toLowerCase()}`, + achievementUuids: [idString], + meta: { + type: GoalType.ASSESSMENT, + assessmentNumber: assessmentOverview.id, + requiredCompletionFrac: 0 + } + }, + assessmentOverview.gradingStatus === 'graded' + ); + // Would like a goal for early submission, but that seems to be hard to get from the overview + inferencer.insertFakeAchievement({ + uuid: idString, + title: assessmentOverview.title, + ability: + assessmentOverview.category === 'Mission' || assessmentOverview.category === 'Path' + ? AchievementAbility.CORE + : AchievementAbility.EFFORT, + xp: + assessmentOverview.gradingStatus === 'graded' + ? assessmentOverview.xp + : assessmentOverview.maxXp, + deadline: new Date(assessmentOverview.closeAt), + release: new Date(assessmentOverview.openAt), + isTask: assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, + position: -1, // always appears on top + prerequisiteUuids: [], + goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete + cardBackground: `${cardBackgroundUrl}/default.png`, + view: { + coverImage: `${coverImageUrl}/default.png`, + description: assessmentOverview.shortSummary, + completionText: `Grade: ${assessmentOverview.grade} / ${assessmentOverview.maxGrade}` + } + }); + } +} + +export default insertFakeAchievements; diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index f5194642b2..bddc1f8d53 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -31,11 +31,13 @@ let inferencer: AchievementInferencer = new AchievementInferencer( store ? store.getState().achievement.goals : [] ); -const goalIncludesEvent = (goal: AchievementGoal, eventName: EventType) => { +function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { if (goal.meta.type === GoalType.EVENT) { for (let i = 0; i < goal.meta.eventNames.length; i++) { - if (goal.meta.eventNames[i] === eventName) { - return true; + for (let j = 0; j < eventNames.length; j++) { + if (goal.meta.eventNames[i] === eventNames[j]) { + return true; + } } } return false; @@ -44,7 +46,7 @@ const goalIncludesEvent = (goal: AchievementGoal, eventName: EventType) => { } }; -export function processEvent(eventName: EventType, increment: number = 1) { +export function processEvent(eventNames: EventType[], increment: number = 1) { // by default, userId should be the current state's one const userId = store.getState().session.userId; // just in case userId is still not defined @@ -56,7 +58,7 @@ export function processEvent(eventName: EventType, increment: number = 1) { // if the inferencer has goals, enter the function body if (goals[0]) { - goals = goals.filter(goal => goalIncludesEvent(goal, eventName)); + goals = goals.filter(goal => goalIncludesEvents(goal, eventNames)); const computeCompleted = (goal: AchievementGoal): boolean => { // all goals that are input as arguments are eventGoals @@ -67,12 +69,13 @@ export function processEvent(eventName: EventType, increment: number = 1) { goal.completed = true; const parentAchievements = inferencer.getAchievementsByGoal(goal.uuid); parentAchievements.forEach(uuid => { - const completed = inferencer - .getAchievement(uuid) - .goalUuids.map(goalUuid => inferencer.getGoal(goalUuid).completed) - .reduce((completion, goalCompletion) => completion && goalCompletion, true); - if (completed) { - showSuccessMessage('Completed acheivement: ' + inferencer.getAchievement(uuid).title); + const achievement = inferencer.getAchievement(uuid); + // something went wrong + if (inferencer.isInvalidAchievement(achievement)) { + return; + } + if (inferencer.isCompleted(achievement)) { + showSuccessMessage('Completed acheivement: ' + achievement.title); } }); return true; @@ -107,7 +110,7 @@ export function processEvent(eventName: EventType, increment: number = 1) { store.getState().achievement.achievements, store.getState().achievement.goals ); - processEvent(eventName, increment); + processEvent(eventNames, increment); }; if (!store.getState().achievement.goals[0]) { diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index 4e404b4f02..7a73e57877 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -21,7 +21,7 @@ export function ControlBarRunButton(props: ControlButtonRunButtonProps) { {controlButton('Run', IconNames.PLAY, () => { props.handleEditorEval(); - processEvent(EventType.RUNCODE); + processEvent([EventType.RUN_CODE]); })} ); diff --git a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx index e5c76456e6..43dbcb9e01 100644 --- a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx +++ b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx @@ -197,10 +197,10 @@ const MobileSideContent: React.FC = props => newTabId === SideContentType.mobileEditorRun ) { props.handleEditorEval(); - processEvent(EventType.RUNCODE); + processEvent([EventType.RUN_CODE]); } else if (newTabId === SideContentType.mobileEditorRun) { props.handleEditorEval(); - processEvent(EventType.RUNCODE); + processEvent([EventType.RUN_CODE]); props.handleShowRepl(); } else { props.handleHideRepl(); diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index e5f10e9b55..c74d99f530 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -22,8 +22,10 @@ import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select, take } from 'redux-saga/effects'; import * as Sourceror from 'sourceror'; +import { EventType } from '../../features/achievement/AchievementTypes'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { DeviceSession } from '../../features/remoteExecution/RemoteExecutionTypes'; +import { processEvent } from '../achievement/utils/eventHandler'; import { OverallState, styliseSublanguage } from '../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../application/types/ExternalTypes'; import { @@ -731,21 +733,25 @@ export function* evalCode( const parsed = parse(code, context); const typeErrors = parsed && typeCheck(validateAndAnnotate(parsed!, context), context)[1]; context.errors = oldErrors; - + // for achievement event tracking + const events = context.errors[0] ? [EventType.ERROR] : []; // report infinite loops but only for 'vanilla'/default source if (context.variant === 'default') { const infiniteLoopData = getInfiniteLoopData(context, code); if (infiniteLoopData) { + events.push(EventType.INFINITE_LOOP); const [error, code] = infiniteLoopData; yield call(reportInfiniteLoopError, error, code); } } if (typeErrors && typeErrors.length > 0) { + events.push(EventType.ERROR); yield put( actions.sendReplInputToOutput('Hints:\n' + parseError(typeErrors), workspaceLocation) ); } + processEvent(events); return; } else if (result.status === 'suspended') { yield put(actions.endDebuggerPause(workspaceLocation)); @@ -765,6 +771,9 @@ export function* evalCode( // For EVAL_EDITOR and EVAL_REPL, we send notification to workspace that a program has been evaluated if (actionType === EVAL_EDITOR || actionType === EVAL_REPL) { + if (context.errors[0]) { + processEvent([EventType.ERROR]); + } yield put(notifyProgramEvaluated(result, lastDebuggerResult, code, context, workspaceLocation)); } diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx index 7e7f8a9460..e23cd065ba 100644 --- a/src/commons/sideContent/SideContent.tsx +++ b/src/commons/sideContent/SideContent.tsx @@ -3,8 +3,6 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import * as React from 'react'; import { useSelector } from 'react-redux'; -import { EventType } from '../../features/achievement/AchievementTypes'; -import { processEvent } from '../achievement/utils/eventHandler'; import { OverallState } from '../application/ApplicationTypes'; import { DebuggerContext, WorkspaceLocation } from '../workspace/WorkspaceTypes'; import { getDynamicTabs } from './SideContentHelper'; @@ -131,8 +129,6 @@ const SideContent = (props: SideContentProps) => { * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs. */ - // Achievements test code! - processEvent(EventType.RUNCODE); const resetAlert = (prevTabId: TabId) => { const iconId = generateIconId(prevTabId); const icon = document.getElementById(iconId); diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 357b65205e..6ec163fe43 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -106,18 +106,28 @@ export const defaultGoalProgress = { }; export enum GoalType { - ASSESSMENT = 'Assessment (unsupported)', - BINARY = 'Binary (unsupported)', MANUAL = 'Manual', - EVENT = 'Event' + EVENT = 'Event', + XP = 'XP', + ACHIEVEMENTS = 'Achievements', + ASSESSMENT = 'Assessment (unsupported)', + BINARY = 'Binary (unsupported)' } export enum EventType { NONE = 'None', // This is just for the purposes of a default value - RUNCODE = 'Run Code' + RUN_CODE = 'Run Code', + ERROR = 'Error', + INFINITE_LOOP = 'Infinite Loop' } -export type GoalMeta = AssessmentMeta | BinaryMeta | ManualMeta | EventMeta; +export type GoalMeta = + | AssessmentMeta + | BinaryMeta + | ManualMeta + | EventMeta + | XpMeta + | AchievementsMeta; export type AssessmentMeta = { type: GoalType.ASSESSMENT; @@ -146,6 +156,16 @@ export type EventMeta = { observeTo?: Date; }; +export type XpMeta = { + type: GoalType.XP; + targetCount: number; +}; + +export type AchievementsMeta = { + type: GoalType.ACHIEVEMENTS; + targetCount: number; +}; + /** * Information of an achievement view * diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 44d21cf859..95a1525649 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -1,5 +1,5 @@ import { IconNames } from '@blueprintjs/icons'; -import { useEffect, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import { Role } from 'src/commons/application/ApplicationTypes'; import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; @@ -9,14 +9,11 @@ import AchievementOverview from '../../../commons/achievement/AchievementOvervie import AchievementTask from '../../../commons/achievement/AchievementTask'; import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; +import insertFakeAchievements from '../../../commons/achievement/utils/InsertFakeAchievements'; import Constants from '../../../commons/utils/Constants'; +import { showSuccessMessage } from '../../../commons/utils/NotificationsHelper'; +import { AchievementContext } from '../../../features/achievement/AchievementConstants'; import { - AchievementContext, - cardBackgroundUrl, - coverImageUrl -} from '../../../features/achievement/AchievementConstants'; -import { - AchievementAbility, AchievementUser, FilterStatus, GoalProgress, @@ -34,6 +31,7 @@ export type DispatchProps = { export type StateProps = { group: string | null; inferencer: AchievementInferencer; + id?: number; name?: string; role?: Role; assessmentOverviews?: AssessmentOverview[]; @@ -68,6 +66,7 @@ function Dashboard(props: DispatchProps & StateProps) { getUsers, updateGoalProgress, fetchAssessmentOverviews, + id, group, inferencer, name, @@ -86,75 +85,17 @@ function Dashboard(props: DispatchProps & StateProps) { } }, [getAchievements, getOwnGoals]); + const [, forceUpdate] = useReducer(x => x + 1, 0); + if (name && role && !assessmentOverviews) { // If assessment overviews are not loaded, fetch them fetchAssessmentOverviews(); } // one goal for submit, one goal for graded - assessmentOverviews?.forEach(assessmentOverview => { - // No goals for contests and practical assessments that don't give XP - if (assessmentOverview.category === 'Contest' || assessmentOverview.category === 'Practical') { - return; - } - const idString = assessmentOverview.id.toString(); - if (!inferencer.hasAchievement(idString)) { - // Goal for assessment submission - inferencer.insertFakeGoalDefinition( - { - uuid: idString + '0', - text: `Submitted ${assessmentOverview.category.toLowerCase()}`, - achievementUuids: [idString], - meta: { - type: GoalType.ASSESSMENT, - assessmentNumber: assessmentOverview.id, - requiredCompletionFrac: 0 - } - }, - assessmentOverview.status === 'submitted' - ); - // Goal for assessment grading - inferencer.insertFakeGoalDefinition( - { - uuid: idString + '1', - text: `Graded ${assessmentOverview.category.toLowerCase()}`, - achievementUuids: [idString], - meta: { - type: GoalType.ASSESSMENT, - assessmentNumber: assessmentOverview.id, - requiredCompletionFrac: 0 - } - }, - assessmentOverview.gradingStatus === 'graded' - ); - // Would like a goal for early submission, but that seems to be hard to get from the overview - inferencer.insertFakeAchievement({ - uuid: idString, - title: assessmentOverview.title, - ability: - assessmentOverview.category === 'Mission' || assessmentOverview.category === 'Path' - ? AchievementAbility.CORE - : AchievementAbility.EFFORT, - xp: - assessmentOverview.gradingStatus === 'graded' - ? assessmentOverview.xp - : assessmentOverview.maxXp, - deadline: new Date(assessmentOverview.closeAt), - release: new Date(assessmentOverview.openAt), - isTask: - assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, - position: -1, // always appears on top - prerequisiteUuids: [], - goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete - cardBackground: `${cardBackgroundUrl}/default.png`, - view: { - coverImage: `${coverImageUrl}/default.png`, - description: assessmentOverview.shortSummary, - completionText: `Grade: ${assessmentOverview.grade} / ${assessmentOverview.maxGrade}` - } - }); - } - }); + assessmentOverviews?.forEach(assessmentOverview => + insertFakeAchievements(assessmentOverview, inferencer) + ); const filterState = useState(FilterStatus.ALL); const [filterStatus] = filterState; @@ -166,6 +107,52 @@ function Dashboard(props: DispatchProps & StateProps) { const focusState = useState(''); const [focusUuid] = focusState; + const completedCount = inferencer.getAllCompletedAchievements().length; + const xp = inferencer.getTotalXp(); + const goals = inferencer.getAllGoals(); + let changed = false; + goals.forEach(goal => { + if (!id) { + return; + } + let update = false; + if (goal.meta.type === GoalType.ACHIEVEMENTS) { + update = goal.count !== completedCount; + goal.count = completedCount; + } + if (goal.meta.type === GoalType.XP) { + update = goal.count !== xp; + goal.count = xp; + } + if (!goal.completed && goal.count >= goal.targetCount) { + goal.completed = true; + const parentAchievements = inferencer.getAchievementsByGoal(goal.uuid); + parentAchievements.forEach(uuid => { + const achievement = inferencer.getAchievement(uuid); + console.log(achievement); + if (inferencer.isInvalidAchievement(achievement)) { + return; + } + if (inferencer.isCompleted(achievement)) { + showSuccessMessage('Completed acheivement: ' + achievement.title); + } + }); + } + changed = changed || update; + if (update) { + const progress: GoalProgress = { + uuid: goal.uuid, + count: goal.count, + targetCount: goal.targetCount, + completed: goal.completed + }; + updateGoalProgress(id, progress); + } + }); + if (changed) { + setTimeout(forceUpdate, 1000); + } + return (
    diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index ca6da2ce7f..05c9ad9dc7 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -19,6 +19,7 @@ const mapStateToProps: MapStateToProps = state => inferencer: Constants.useAchievementBackend ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) : new AchievementInferencer(mockAchievements, mockGoals), + id: state.session.userId, name: state.session.name, role: state.session.role, assessmentOverviews: state.session.assessmentOverviews, From afc7243f848c4020c38810261c0255474d8190fd Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 15:48:26 +0800 Subject: [PATCH 118/143] Fixed formatting. --- src/commons/achievement/utils/eventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index bddc1f8d53..18b882c465 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -44,7 +44,7 @@ function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { } else { return false; } -}; +} export function processEvent(eventNames: EventType[], increment: number = 1) { // by default, userId should be the current state's one From 23762d3ccc6d8561e2d71a6d6147377d24202d58 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 17:34:28 +0800 Subject: [PATCH 119/143] Added variable XP achievements. Also made it so that assessment achievements do not get generated if their deadline is over and have not been submitted. --- .../achievement/AchievementManualEditor.tsx | 1 - .../achievementEditor/AchievementSettings.tsx | 14 +++++++++++++- .../achievementEditor/AchievementTemplate.ts | 1 + .../control/achievementEditor/EditableCard.tsx | 11 +++++++++++ .../achievementEditor/EditableCardTypes.ts | 4 ++++ .../achievement/utils/AchievementBackender.ts | 1 + .../achievement/utils/AchievementInferencer.ts | 18 ++++++++++++++---- .../utils/InsertFakeAchievements.ts | 9 +++++++-- .../__tests__/AchievementInferencer.test.ts | 1 + src/commons/mocks/AchievementMocks.ts | 12 ++++++++++++ src/features/achievement/AchievementTypes.ts | 1 + 11 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index 29426562af..2fb98fb5e2 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -98,7 +98,6 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { void; changePosition: (position: number) => void; changePrerequisiteUuids: (prerequisiteUuids: string[]) => void; + changeVariableXp: () => void; editableAchievement: AchievementItem; }; @@ -22,9 +23,17 @@ function AchievementSettings(props: AchievementSettingsProps) { changeGoalUuids, changePosition, changePrerequisiteUuids, + changeVariableXp, editableAchievement } = props; - const { uuid, cardBackground, goalUuids, position, prerequisiteUuids } = editableAchievement; + const { + uuid, + cardBackground, + goalUuids, + position, + prerequisiteUuids, + variableXp + } = editableAchievement; const [isOpen, setOpen] = useState(false); const toggleOpen = () => setOpen(!isOpen); @@ -55,6 +64,9 @@ function AchievementSettings(props: AchievementSettingsProps) { />

    Goals

    + +
    diff --git a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts index 1ce6d37f9b..1cb21599fd 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts +++ b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts @@ -19,6 +19,7 @@ export const achievementTemplate: AchievementItem = { title: 'Achievement Title Here', ability: AchievementAbility.CORE, xp: 0, + variableXp: false, isTask: false, position: 0, prerequisiteUuids: [], diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index e9641bfd2c..7084f8d148 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -130,6 +130,14 @@ const reducer = (state: State, action: Action) => { }, isDirty: true }; + case ActionType.CHANGE_VARIABLE_XP: + return { + editableAchievement: { + ...state.editableAchievement, + variableXp: !state.editableAchievement.variableXp + }, + isDirty: true + }; default: return state; } @@ -205,6 +213,8 @@ function EditableCard(props: EditableCardProps) { const changeXp = (xp: number) => dispatch({ type: ActionType.CHANGE_XP, payload: xp }); + const changeVariableXp = () => dispatch({ type: ActionType.CHANGE_VARIABLE_XP }); + return (
  • diff --git a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts index a04ba73cc9..dff5f70a6f 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts +++ b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts @@ -13,6 +13,7 @@ export enum EditableCardActionType { CHANGE_PREREQUISITE_UUIDS = 'CHANGE_PREREQUISITE_UUIDS', CHANGE_RELEASE = 'CHANGE_RELEASE', CHANGE_TITLE = 'CHANGE_TITLE', + CHANGE_VARIABLE_XP = 'CHANGE_VARIABLE_XP', CHANGE_VIEW = 'CHANGE_VIEW', CHANGE_XP = 'CHANGE_XP', DELETE_ACHIEVEMENT = 'DELETE_ACHIEVEMENT', @@ -53,6 +54,9 @@ export type EditableCardAction = type: EditableCardActionType.CHANGE_TITLE; payload: string; } + | { + type: EditableCardActionType.CHANGE_VARIABLE_XP; + } | { type: EditableCardActionType.CHANGE_VIEW; payload: AchievementView; diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 8e769c24a4..5957c5c063 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -41,6 +41,7 @@ export const frontendifyAchievementItem = (achievement: any) => title: achievement.title || '', ability: achievement.ability as AchievementAbility, xp: achievement.xp, + variableXp: achievement.variableXp, deadline: achievement.deadline === null ? undefined : new Date(achievement.deadline), release: achievement.release === null ? undefined : new Date(achievement.release), isTask: achievement.isTask, diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 7a4a0dd134..2cafb02c4e 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -87,6 +87,7 @@ class AchievementInferencer { title: 'invalid', ability: AchievementAbility.EXPLORATION, xp: 0, + variableXp: false, deadline: undefined, release: undefined, isTask: false, @@ -557,7 +558,12 @@ class AchievementInferencer { * @param uuid Achievement Uuid */ public getAchievementXp(uuid: string) { - return this.getAchievement(uuid).xp; + const achievement = this.getAchievement(uuid); + if (achievement.variableXp) { + return this.listGoals(achievement.uuid).reduce((xp, goal) => xp + goal.count, 0); + } else { + return achievement.xp; + } } /** @@ -565,7 +571,7 @@ class AchievementInferencer { */ public getTotalXp() { return this.getAllCompletedAchievements().reduce( - (totalXp, achievement) => totalXp + achievement.xp, + (totalXp, achievement) => totalXp + this.getAchievementXp(achievement.uuid), 0 ); } @@ -768,12 +774,16 @@ class AchievementInferencer { * @param node the AchievementNode */ private generateXp(node: AchievementNode) { - const { goalUuids } = node.achievement; + const { goalUuids, variableXp } = node.achievement; const allGoalsCompleted = goalUuids.reduce( (completion, goalUuid) => completion && this.getGoal(goalUuid).completed, true ); - node.xp = allGoalsCompleted ? node.achievement.xp : 0; + node.xp = allGoalsCompleted + ? variableXp + ? goalUuids.reduce((xp, goalUuid) => xp + this.getGoal(goalUuid).count, 0) + : node.achievement.xp + : 0; } /** diff --git a/src/commons/achievement/utils/InsertFakeAchievements.ts b/src/commons/achievement/utils/InsertFakeAchievements.ts index 5ab49745f5..a9cc5f2162 100644 --- a/src/commons/achievement/utils/InsertFakeAchievements.ts +++ b/src/commons/achievement/utils/InsertFakeAchievements.ts @@ -5,13 +5,17 @@ import { import { AchievementAbility, GoalType } from '../../../features/achievement/AchievementTypes'; import { AssessmentOverview } from '../../assessment/AssessmentTypes'; import AchievementInferencer from './AchievementInferencer'; +import { isExpired, isReleased } from './DateHelper'; function insertFakeAchievements( assessmentOverview: AssessmentOverview, inferencer: AchievementInferencer ) { - // No goals for contests and practical assessments that don't give XP - if (assessmentOverview.category === 'Contest' || assessmentOverview.category === 'Practical') { + // Reduce clutter for achievements that cannot be earned at that point + if ( + !isReleased(new Date(assessmentOverview.openAt)) || + (isExpired(new Date(assessmentOverview.closeAt)) && assessmentOverview.status !== 'submitted') + ) { return; } const idString = assessmentOverview.id.toString(); @@ -56,6 +60,7 @@ function insertFakeAchievements( assessmentOverview.gradingStatus === 'graded' ? assessmentOverview.xp : assessmentOverview.maxXp, + variableXp: false, deadline: new Date(assessmentOverview.closeAt), release: new Date(assessmentOverview.openAt), isTask: assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 264d414d7a..ff21ad5309 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -17,6 +17,7 @@ const testAchievement: AchievementItem = { title: 'Test Achievement', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, isTask: false, prerequisiteUuids: [], goalUuids: [], diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index 8fd77b06d5..67f39d1512 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -12,6 +12,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Rune Master', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, isTask: true, position: 1, prerequisiteUuids: ['2', '1'], @@ -31,6 +32,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Beyond the Second Dimension', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, deadline: new Date(2020, 7, 6, 12, 30, 0), isTask: false, position: 0, @@ -51,6 +53,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Colorful Carpet', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, deadline: new Date(2020, 7, 8, 9, 0, 0), isTask: false, position: 0, @@ -71,6 +74,7 @@ export const mockAchievements: AchievementItem[] = [ title: '', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, isTask: false, position: 0, prerequisiteUuids: [], @@ -87,6 +91,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Wizard', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, deadline: new Date(2020, 8, 15, 0, 0, 0), isTask: true, position: 4, @@ -107,6 +112,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Introduction', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, deadline: new Date(2020, 7, 28, 0, 0, 0), isTask: false, position: 0, @@ -127,6 +133,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Manipulation', ability: AchievementAbility.CORE, xp: 100, + variableXp: false, deadline: new Date(2020, 8, 5, 0, 0, 0), isTask: false, position: 0, @@ -147,6 +154,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'The Source-rer', ability: AchievementAbility.EFFORT, xp: 100, + variableXp: false, deadline: new Date(2020, 7, 21, 0, 0, 0), isTask: true, position: 3, @@ -167,6 +175,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Power of Friendship', ability: AchievementAbility.COMMUNITY, xp: 100, + variableXp: false, isTask: true, position: 2, prerequisiteUuids: ['9'], @@ -186,6 +195,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Piazza Guru', ability: AchievementAbility.COMMUNITY, xp: 100, + variableXp: false, isTask: false, position: 0, prerequisiteUuids: [], @@ -205,6 +215,7 @@ export const mockAchievements: AchievementItem[] = [ title: "That's the Spirit", ability: AchievementAbility.EXPLORATION, xp: 100, + variableXp: false, isTask: true, position: 5, prerequisiteUuids: [], @@ -224,6 +235,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Kool Kidz', ability: AchievementAbility.FLEX, xp: 100, + variableXp: false, isTask: true, position: 6, prerequisiteUuids: [], diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 6ec163fe43..b5c98ed266 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -57,6 +57,7 @@ export type AchievementItem = { title: string; ability: AchievementAbility; xp: number; + variableXp: boolean; deadline?: Date; release?: Date; isTask: boolean; From 3ec298e805a5ec1b5437f22359e5ff68cf37f7dd Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 19:53:29 +0800 Subject: [PATCH 120/143] Added infinite loop check in eventHandler --- src/commons/achievement/utils/eventHandler.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 18b882c465..e0f14e47e1 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -46,7 +46,7 @@ function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { } } -export function processEvent(eventNames: EventType[], increment: number = 1) { +export function processEvent(eventNames: EventType[], increment: number = 1, retry: boolean = false) { // by default, userId should be the current state's one const userId = store.getState().session.userId; // just in case userId is still not defined @@ -57,7 +57,8 @@ export function processEvent(eventNames: EventType[], increment: number = 1) { let goals = inferencer.getAllGoals(); // if the inferencer has goals, enter the function body - if (goals[0]) { + // if this is the retry, also enter the function body to prevent infinite loops + if (goals[0] || retry) { goals = goals.filter(goal => goalIncludesEvents(goal, eventNames)); const computeCompleted = (goal: AchievementGoal): boolean => { @@ -110,7 +111,7 @@ export function processEvent(eventNames: EventType[], increment: number = 1) { store.getState().achievement.achievements, store.getState().achievement.goals ); - processEvent(eventNames, increment); + processEvent(eventNames, increment, true); }; if (!store.getState().achievement.goals[0]) { From 4fbf6244b422146982b4989e9bb17ca39f737783 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 19:54:34 +0800 Subject: [PATCH 121/143] Fixed formatting. --- src/commons/achievement/utils/eventHandler.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index e0f14e47e1..756e5801db 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -46,7 +46,11 @@ function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { } } -export function processEvent(eventNames: EventType[], increment: number = 1, retry: boolean = false) { +export function processEvent( + eventNames: EventType[], + increment: number = 1, + retry: boolean = false +) { // by default, userId should be the current state's one const userId = store.getState().session.userId; // just in case userId is still not defined From 7bfc6774248f5ba18bfb8d0c4ed96cb8ec8795f5 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 21:33:47 +0800 Subject: [PATCH 122/143] Fixed bug where invalid goal stays on when deleted --- .../achievementSettings/EditableGoalUuids.tsx | 6 +++++- .../achievementSettings/EditablePrerequisiteUuids.tsx | 6 +++++- src/pages/achievement/control/AchievementControl.tsx | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalUuids.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalUuids.tsx index 27b9fe8c6d..dc955c793d 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalUuids.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalUuids.tsx @@ -14,6 +14,10 @@ function EditableGoalUuids(props: EditableGoalUuidsProps) { const inferencer = useContext(AchievementContext); const allGoalUuids = inferencer.getAllGoalUuids(); + const selectedUuids = goalUuids.filter( + uuid => !inferencer.isInvalidGoal(inferencer.getGoal(uuid)) + ); + const getUuid = (text: string) => inferencer.getUuidByText(text); const getText = (uuid: string) => inferencer.getTextByUuid(uuid); @@ -22,7 +26,7 @@ function EditableGoalUuids(props: EditableGoalUuidsProps) { ); - const selectedGoals = new Set(goalUuids); + const selectedGoals = new Set(selectedUuids); const availableGoals = new Set(without(allGoalUuids, ...goalUuids)); const selectGoal = (selectUuid: string) => { diff --git a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteUuids.tsx b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteUuids.tsx index e103fefc73..88336f2a6b 100644 --- a/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteUuids.tsx +++ b/src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteUuids.tsx @@ -14,6 +14,10 @@ function EditablePrerequisiteUuids(props: EditablePrerequisiteUuidsProps) { const inferencer = useContext(AchievementContext); const availableUuids = inferencer.listAvailablePrerequisiteUuids(uuid); + const selectedUuids = prerequisiteUuids.filter( + uuid => !inferencer.isInvalidAchievement(inferencer.getAchievement(uuid)) + ); + const getUuid = (title: string) => inferencer.getUuidByTitle(title); const getTitle = (uuid: string) => inferencer.getTitleByUuid(uuid); @@ -22,7 +26,7 @@ function EditablePrerequisiteUuids(props: EditablePrerequisiteUuidsProps) { ); - const selectedPrereqs = new Set(prerequisiteUuids); + const selectedPrereqs = new Set(selectedUuids); const availablePrereqs = new Set(availableUuids); const selectPrereq = (selectUuid: string) => { diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index a0ac0486d7..e1230dbf1a 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -48,11 +48,11 @@ function AchievementControl(props: DispatchProps & StateProps) { */ const [awaitPublish, setAwaitPublish] = useState(false); const publishChanges = () => { - // NOTE: Update goals first because goals must exist before their UUID can be specified in achievements + // NOTE: Goals and achievements must exist in the backend before the association can be built bulkUpdateGoals(inferencer.getAllGoals()); bulkUpdateAchievements(inferencer.getAllAchievements()); - inferencer.getAchievementsToDelete().forEach(removeAchievement); inferencer.getGoalsToDelete().forEach(removeGoal); + inferencer.getAchievementsToDelete().forEach(removeAchievement); inferencer.resetToDelete(); setAwaitPublish(false); }; From 0e4915c2b42e0028557a047350187668ffe9eee1 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Fri, 16 Apr 2021 23:02:12 +0800 Subject: [PATCH 123/143] Fixed AchievementManualEditor redundant code. --- .../achievement/AchievementManualEditor.tsx | 85 ++++++++----------- .../subcomponents/AchievementDashboard.tsx | 4 +- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index 2fb98fb5e2..a83cd2bbfa 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -30,7 +30,7 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { .filter(user => user.group === studio) .sort((user1, user2) => user1.name.localeCompare(user2.name)); - useEffect(() => getUsers(), [getUsers]); + useEffect(getUsers, [getUsers]); const inferencer = useContext(AchievementContext); const manualAchievements: AchievementGoal[] = inferencer @@ -58,56 +58,45 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { } }; - if (studio !== 'Staff') { - // TODO - // For the studio's avenger to manually assign to his students - // In theory, just copy paste the bottom code, but make userID a select - return ( -
    -

    {studio}

    -
    - ); - } else { - return ( -
    -

    User:

    - -
    - ); - } +

    +
    + ); } export default AchievementManualEditor; diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 95a1525649..7d7759c919 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -110,6 +110,8 @@ function Dashboard(props: DispatchProps & StateProps) { const completedCount = inferencer.getAllCompletedAchievements().length; const xp = inferencer.getTotalXp(); const goals = inferencer.getAllGoals(); + + // Computes goal progress for achievement and xp goals let changed = false; goals.forEach(goal => { if (!id) { @@ -157,7 +159,7 @@ function Dashboard(props: DispatchProps & StateProps) {
    - {role !== Role.Student && ( + {(role && role !== Role.Student) && ( Date: Fri, 16 Apr 2021 23:03:46 +0800 Subject: [PATCH 124/143] Fixed formatting. --- src/pages/achievement/subcomponents/AchievementDashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 7d7759c919..105000ece7 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -159,7 +159,7 @@ function Dashboard(props: DispatchProps & StateProps) {
    - {(role && role !== Role.Student) && ( + {role && role !== Role.Student && ( Date: Sat, 17 Apr 2021 00:10:52 +0800 Subject: [PATCH 125/143] Added Run Testcase Event Type. --- src/commons/achievement/utils/eventHandler.ts | 5 +---- src/commons/controlBar/ControlBarRunButton.tsx | 7 +------ .../mobileSideContent/MobileSideContent.tsx | 4 ---- src/commons/sagas/WorkspaceSaga.ts | 3 +++ src/features/achievement/AchievementTypes.ts | 3 ++- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 756e5801db..2a39d92fdc 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -26,10 +26,7 @@ function eventShouldCount(meta: EventMeta): boolean { return false; } -let inferencer: AchievementInferencer = new AchievementInferencer( - store ? store.getState().achievement.achievements : [], - store ? store.getState().achievement.goals : [] -); +let inferencer: AchievementInferencer = new AchievementInferencer([], []); function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { if (goal.meta.type === GoalType.EVENT) { diff --git a/src/commons/controlBar/ControlBarRunButton.tsx b/src/commons/controlBar/ControlBarRunButton.tsx index 7a73e57877..b624f4163b 100644 --- a/src/commons/controlBar/ControlBarRunButton.tsx +++ b/src/commons/controlBar/ControlBarRunButton.tsx @@ -2,8 +2,6 @@ import { Position } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; -import { EventType } from '../../features/achievement/AchievementTypes'; -import { processEvent } from '../achievement/utils/eventHandler'; import controlButton from '../ControlButton'; type ControlButtonRunButtonProps = DispatchProps & StateProps; @@ -19,10 +17,7 @@ type StateProps = { export function ControlBarRunButton(props: ControlButtonRunButtonProps) { return ( - {controlButton('Run', IconNames.PLAY, () => { - props.handleEditorEval(); - processEvent([EventType.RUN_CODE]); - })} + {controlButton('Run', IconNames.PLAY, props.handleEditorEval)} ); } diff --git a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx index 43dbcb9e01..8f0329cc69 100644 --- a/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx +++ b/src/commons/mobileWorkspace/mobileSideContent/MobileSideContent.tsx @@ -3,8 +3,6 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import React from 'react'; import { useSelector } from 'react-redux'; -import { EventType } from '../../../features/achievement/AchievementTypes'; -import { processEvent } from '../../achievement/utils/eventHandler'; import { OverallState } from '../../application/ApplicationTypes'; import { ControlBarProps } from '../../controlBar/ControlBar'; import { SideContentTab, SideContentType } from '../../sideContent/SideContentTypes'; @@ -197,10 +195,8 @@ const MobileSideContent: React.FC = props => newTabId === SideContentType.mobileEditorRun ) { props.handleEditorEval(); - processEvent([EventType.RUN_CODE]); } else if (newTabId === SideContentType.mobileEditorRun) { props.handleEditorEval(); - processEvent([EventType.RUN_CODE]); props.handleShowRepl(); } else { props.handleHideRepl(); diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index c74d99f530..e6b3906cb1 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -118,6 +118,8 @@ export default function* WorkspaceSaga(): SagaIterator { globals }; + processEvent([EventType.RUN_CODE]); + if (remoteExecutionSession && remoteExecutionSession.workspace === workspaceLocation) { yield put(actions.remoteExecRun(editorCode)); } else { @@ -299,6 +301,7 @@ export default function* WorkspaceSaga(): SagaIterator { ); yield takeEvery(EVAL_TESTCASE, function* (action: ReturnType) { + processEvent([EventType.RUN_CODE, EventType.RUN_TESTCASE]); const workspaceLocation = action.payload.workspaceLocation; const index = action.payload.testcaseId; const code: string = yield select((state: OverallState) => { diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index b5c98ed266..836bccd110 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -119,7 +119,8 @@ export enum EventType { NONE = 'None', // This is just for the purposes of a default value RUN_CODE = 'Run Code', ERROR = 'Error', - INFINITE_LOOP = 'Infinite Loop' + INFINITE_LOOP = 'Infinite Loop', + RUN_TESTCASE = 'Run Testcase' } export type GoalMeta = From 69b4278cf99fb0db8b0bad57a7cff4b9f69e5281 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Sat, 17 Apr 2021 00:25:29 +0800 Subject: [PATCH 126/143] Revert an accidentally added newline. --- src/commons/sideContent/SideContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx index e23cd065ba..7b77dae29a 100644 --- a/src/commons/sideContent/SideContent.tsx +++ b/src/commons/sideContent/SideContent.tsx @@ -128,7 +128,6 @@ const SideContent = (props: SideContentProps) => { * To be run when tabs are changed. * Currently this style is only used for the "Inspector" and "Env Visualizer" tabs. */ - const resetAlert = (prevTabId: TabId) => { const iconId = generateIconId(prevTabId); const icon = document.getElementById(iconId); From b6b0c0219b26aa242978c70834c8d580d8c855a0 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Sun, 18 Apr 2021 11:04:13 +0800 Subject: [PATCH 127/143] Fixed backend endpoints. Split updateGoalProgress into self and admin, with validation. Renamed variableXp to isVariableXp Changed some critical endpoints to comply with v2 backend. --- .../achievementEditor/AchievementSettings.tsx | 8 ++--- .../achievementEditor/AchievementTemplate.ts | 2 +- .../achievementEditor/EditableCard.tsx | 8 ++--- .../achievementEditor/EditableCardTypes.ts | 4 +-- .../achievement/utils/AchievementBackender.ts | 2 +- .../utils/AchievementInferencer.ts | 8 ++--- .../utils/InsertFakeAchievements.ts | 2 +- .../__tests__/AchievementInferencer.test.ts | 2 +- src/commons/achievement/utils/eventHandler.ts | 4 +-- src/commons/mocks/AchievementMocks.ts | 24 +++++++-------- src/commons/sagas/AchievementSaga.ts | 24 +++++++++++++-- src/commons/sagas/RequestsSaga.ts | 30 +++++++++++++++---- .../achievement/AchievementActions.ts | 12 +++++--- src/features/achievement/AchievementTypes.ts | 3 +- src/features/game/save/GameSaveRequests.ts | 4 +-- .../storySimulator/StorySimulatorRequest.ts | 2 +- 16 files changed, 91 insertions(+), 48 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index 4aaa3b3411..14cb552097 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -13,7 +13,7 @@ type AchievementSettingsProps = { changeGoalUuids: (goalUuids: string[]) => void; changePosition: (position: number) => void; changePrerequisiteUuids: (prerequisiteUuids: string[]) => void; - changeVariableXp: () => void; + changeIsVariableXp: () => void; editableAchievement: AchievementItem; }; @@ -23,7 +23,7 @@ function AchievementSettings(props: AchievementSettingsProps) { changeGoalUuids, changePosition, changePrerequisiteUuids, - changeVariableXp, + changeIsVariableXp, editableAchievement } = props; const { @@ -32,7 +32,7 @@ function AchievementSettings(props: AchievementSettingsProps) { goalUuids, position, prerequisiteUuids, - variableXp + isVariableXp } = editableAchievement; const [isOpen, setOpen] = useState(false); @@ -65,7 +65,7 @@ function AchievementSettings(props: AchievementSettingsProps) {

    Goals

    -
    diff --git a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts index 1cb21599fd..836ebba849 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts +++ b/src/commons/achievement/control/achievementEditor/AchievementTemplate.ts @@ -19,7 +19,7 @@ export const achievementTemplate: AchievementItem = { title: 'Achievement Title Here', ability: AchievementAbility.CORE, xp: 0, - variableXp: false, + isVariableXp: false, isTask: false, position: 0, prerequisiteUuids: [], diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 7084f8d148..f63ee2db41 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -130,11 +130,11 @@ const reducer = (state: State, action: Action) => { }, isDirty: true }; - case ActionType.CHANGE_VARIABLE_XP: + case ActionType.CHANGE_IS_VARIABLE_XP: return { editableAchievement: { ...state.editableAchievement, - variableXp: !state.editableAchievement.variableXp + isVariableXp: !state.editableAchievement.isVariableXp }, isDirty: true }; @@ -213,7 +213,7 @@ function EditableCard(props: EditableCardProps) { const changeXp = (xp: number) => dispatch({ type: ActionType.CHANGE_XP, payload: xp }); - const changeVariableXp = () => dispatch({ type: ActionType.CHANGE_VARIABLE_XP }); + const changeIsVariableXp = () => dispatch({ type: ActionType.CHANGE_IS_VARIABLE_XP }); return (
  • diff --git a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts index dff5f70a6f..2bd1f66d00 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts +++ b/src/commons/achievement/control/achievementEditor/EditableCardTypes.ts @@ -13,7 +13,7 @@ export enum EditableCardActionType { CHANGE_PREREQUISITE_UUIDS = 'CHANGE_PREREQUISITE_UUIDS', CHANGE_RELEASE = 'CHANGE_RELEASE', CHANGE_TITLE = 'CHANGE_TITLE', - CHANGE_VARIABLE_XP = 'CHANGE_VARIABLE_XP', + CHANGE_IS_VARIABLE_XP = 'CHANGE_VARIABLE_XP', CHANGE_VIEW = 'CHANGE_VIEW', CHANGE_XP = 'CHANGE_XP', DELETE_ACHIEVEMENT = 'DELETE_ACHIEVEMENT', @@ -55,7 +55,7 @@ export type EditableCardAction = payload: string; } | { - type: EditableCardActionType.CHANGE_VARIABLE_XP; + type: EditableCardActionType.CHANGE_IS_VARIABLE_XP; } | { type: EditableCardActionType.CHANGE_VIEW; diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 5957c5c063..0d63b3ae24 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -41,7 +41,7 @@ export const frontendifyAchievementItem = (achievement: any) => title: achievement.title || '', ability: achievement.ability as AchievementAbility, xp: achievement.xp, - variableXp: achievement.variableXp, + isVariableXp: achievement.isVariableXp, deadline: achievement.deadline === null ? undefined : new Date(achievement.deadline), release: achievement.release === null ? undefined : new Date(achievement.release), isTask: achievement.isTask, diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 2cafb02c4e..df1ca59ca6 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -87,7 +87,7 @@ class AchievementInferencer { title: 'invalid', ability: AchievementAbility.EXPLORATION, xp: 0, - variableXp: false, + isVariableXp: false, deadline: undefined, release: undefined, isTask: false, @@ -559,7 +559,7 @@ class AchievementInferencer { */ public getAchievementXp(uuid: string) { const achievement = this.getAchievement(uuid); - if (achievement.variableXp) { + if (achievement.isVariableXp) { return this.listGoals(achievement.uuid).reduce((xp, goal) => xp + goal.count, 0); } else { return achievement.xp; @@ -774,13 +774,13 @@ class AchievementInferencer { * @param node the AchievementNode */ private generateXp(node: AchievementNode) { - const { goalUuids, variableXp } = node.achievement; + const { goalUuids, isVariableXp } = node.achievement; const allGoalsCompleted = goalUuids.reduce( (completion, goalUuid) => completion && this.getGoal(goalUuid).completed, true ); node.xp = allGoalsCompleted - ? variableXp + ? isVariableXp ? goalUuids.reduce((xp, goalUuid) => xp + this.getGoal(goalUuid).count, 0) : node.achievement.xp : 0; diff --git a/src/commons/achievement/utils/InsertFakeAchievements.ts b/src/commons/achievement/utils/InsertFakeAchievements.ts index a9cc5f2162..1830a0f97c 100644 --- a/src/commons/achievement/utils/InsertFakeAchievements.ts +++ b/src/commons/achievement/utils/InsertFakeAchievements.ts @@ -60,7 +60,7 @@ function insertFakeAchievements( assessmentOverview.gradingStatus === 'graded' ? assessmentOverview.xp : assessmentOverview.maxXp, - variableXp: false, + isVariableXp: false, deadline: new Date(assessmentOverview.closeAt), release: new Date(assessmentOverview.openAt), isTask: assessmentOverview.isPublished === undefined ? true : assessmentOverview.isPublished, diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index ff21ad5309..f7b68428f9 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -17,7 +17,7 @@ const testAchievement: AchievementItem = { title: 'Test Achievement', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, isTask: false, prerequisiteUuids: [], goalUuids: [], diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 2a39d92fdc..70af0ceeb0 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -1,7 +1,7 @@ import { getAchievements, getOwnGoals, - updateGoalProgress + updateOwnGoalProgress } from '../../../features/achievement/AchievementActions'; import { AchievementGoal, @@ -102,7 +102,7 @@ export function processEvent( }; // update goal progress in the backend - userId && store.dispatch(updateGoalProgress(userId, progress)); + userId && store.dispatch(updateOwnGoalProgress(progress)); } }); // if goals are not in state, load the goals from the backend and try again diff --git a/src/commons/mocks/AchievementMocks.ts b/src/commons/mocks/AchievementMocks.ts index 67f39d1512..dea737e283 100644 --- a/src/commons/mocks/AchievementMocks.ts +++ b/src/commons/mocks/AchievementMocks.ts @@ -12,7 +12,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Rune Master', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, isTask: true, position: 1, prerequisiteUuids: ['2', '1'], @@ -32,7 +32,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Beyond the Second Dimension', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 7, 6, 12, 30, 0), isTask: false, position: 0, @@ -53,7 +53,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Colorful Carpet', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 7, 8, 9, 0, 0), isTask: false, position: 0, @@ -74,7 +74,7 @@ export const mockAchievements: AchievementItem[] = [ title: '', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, isTask: false, position: 0, prerequisiteUuids: [], @@ -91,7 +91,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Wizard', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 8, 15, 0, 0, 0), isTask: true, position: 4, @@ -112,7 +112,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Introduction', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 7, 28, 0, 0, 0), isTask: false, position: 0, @@ -133,7 +133,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Curve Manipulation', ability: AchievementAbility.CORE, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 8, 5, 0, 0, 0), isTask: false, position: 0, @@ -154,7 +154,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'The Source-rer', ability: AchievementAbility.EFFORT, xp: 100, - variableXp: false, + isVariableXp: false, deadline: new Date(2020, 7, 21, 0, 0, 0), isTask: true, position: 3, @@ -175,7 +175,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Power of Friendship', ability: AchievementAbility.COMMUNITY, xp: 100, - variableXp: false, + isVariableXp: false, isTask: true, position: 2, prerequisiteUuids: ['9'], @@ -195,7 +195,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Piazza Guru', ability: AchievementAbility.COMMUNITY, xp: 100, - variableXp: false, + isVariableXp: false, isTask: false, position: 0, prerequisiteUuids: [], @@ -215,7 +215,7 @@ export const mockAchievements: AchievementItem[] = [ title: "That's the Spirit", ability: AchievementAbility.EXPLORATION, xp: 100, - variableXp: false, + isVariableXp: false, isTask: true, position: 5, prerequisiteUuids: [], @@ -235,7 +235,7 @@ export const mockAchievements: AchievementItem[] = [ title: 'Kool Kidz', ability: AchievementAbility.FLEX, xp: 100, - variableXp: false, + isVariableXp: false, isTask: true, position: 6, prerequisiteUuids: [], diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index 2c4e2cc959..c1c214a90b 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -12,7 +12,8 @@ import { GET_USERS, REMOVE_ACHIEVEMENT, REMOVE_GOAL, - UPDATE_GOAL_PROGRESS + UPDATE_GOAL_PROGRESS, + UPDATE_OWN_GOAL_PROGRESS } from '../../features/achievement/AchievementTypes'; import { OverallState } from '../application/ApplicationTypes'; import { actions } from '../utils/ActionsHelper'; @@ -27,7 +28,8 @@ import { getOwnGoals, removeAchievement, removeGoal, - updateGoalProgress + updateGoalProgress, + updateOwnGoalProgress } from './RequestsSaga'; import { safeTakeEvery as takeEvery } from './SafeEffects'; @@ -185,6 +187,24 @@ export default function* AchievementSaga(): SagaIterator { } }); + yield takeEvery( + UPDATE_OWN_GOAL_PROGRESS, + function* (action: ReturnType) { + const tokens = yield select((state: OverallState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + + const progress = action.payload; + + const resp = yield call(updateOwnGoalProgress, progress, tokens); + + if (!resp) { + return; + } + } + ); + yield takeEvery( UPDATE_GOAL_PROGRESS, function* (action: ReturnType) { diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index ec6a35956c..2a88c0b819 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -69,7 +69,7 @@ export const postAuth = async ( clientId?: string, redirectUri?: string ): Promise => { - const resp = await request('auth', 'POST', { + const resp = await request('auth/login', 'POST', { body: { code, provider: providerId, @@ -286,14 +286,32 @@ export const editGoal = async ( }; /** - * POST /achievements/goals/{goalUuid}/{studentId} + * POST /self/goals/{goalUuid}/progress + */ + export const updateOwnGoalProgress = async ( + progress: GoalProgress, + tokens: Tokens +): Promise => { + const resp = await request(`self/goals/${progress.uuid}/progress`, 'POST', { + ...tokens, + body: { progress: progress }, + noHeaderAccept: true, + shouldAutoLogout: false, + shouldRefresh: true + }); + + return resp; +}; + +/** + * POST /admin/users/{studentId}/goals/{goalUuid}/progress */ export const updateGoalProgress = async ( studentId: number, progress: GoalProgress, tokens: Tokens ): Promise => { - const resp = await request(`achievements/goals/${progress.uuid}/${studentId}`, 'POST', { + const resp = await request(`admin/users/${studentId}/goals/${progress.uuid}/progress`, 'POST', { ...tokens, body: { progress: progress }, noHeaderAccept: true, @@ -669,7 +687,7 @@ export const postUnsubmit = async ( * GET /notification */ export const getNotifications = async (tokens: Tokens): Promise => { - const resp: Response | null = await request('notification', 'GET', { + const resp: Response | null = await request('notifications', 'GET', { ...tokens, shouldAutoLogout: false }); @@ -705,7 +723,7 @@ export const postAcknowledgeNotifications = async ( tokens: Tokens, ids: number[] ): Promise => { - const resp: Response | null = await request('notification/acknowledge', 'POST', { + const resp: Response | null = await request('notifications/acknowledge', 'POST', { ...tokens, body: { notificationIds: ids }, shouldAutoLogout: false @@ -1065,7 +1083,7 @@ export const request = async ( } try { - const resp = await fetch(`${Constants.backendUrl}/v1/${path}`, fetchOpts); + const resp = await fetch(`${Constants.backendUrl}/v2/${path}`, fetchOpts); // response.ok is (200 <= response.status <= 299) // response.status of > 299 does not raise error; so deal with in in the try clause diff --git a/src/features/achievement/AchievementActions.ts b/src/features/achievement/AchievementActions.ts index aa5d981f6f..bc391c6be4 100644 --- a/src/features/achievement/AchievementActions.ts +++ b/src/features/achievement/AchievementActions.ts @@ -19,7 +19,8 @@ import { SAVE_ACHIEVEMENTS, SAVE_GOALS, SAVE_USERS, - UPDATE_GOAL_PROGRESS + UPDATE_GOAL_PROGRESS, + UPDATE_OWN_GOAL_PROGRESS } from './AchievementTypes'; export const bulkUpdateAchievements = (achievements: AchievementItem[]) => @@ -44,6 +45,12 @@ export const removeAchievement = (uuid: string) => action(REMOVE_ACHIEVEMENT, uu export const removeGoal = (uuid: string) => action(REMOVE_GOAL, uuid); +export const updateOwnGoalProgress = (progress: GoalProgress) => + action(UPDATE_OWN_GOAL_PROGRESS, progress); + +export const updateGoalProgress = (studentId: number, progress: GoalProgress) => + action(UPDATE_GOAL_PROGRESS, { studentId, progress }); + /* Note: This updates the frontend Achievement Redux store. Please refer to AchievementReducer to find out more. @@ -62,6 +69,3 @@ export const saveGoals = (goals: AchievementGoal[]) => action(SAVE_GOALS, goals) Please refer to AchievementReducer to find out more. */ export const saveUsers = (users: AchievementUser[]) => action(SAVE_USERS, users); - -export const updateGoalProgress = (studentId: number, progress: GoalProgress) => - action(UPDATE_GOAL_PROGRESS, { studentId, progress }); diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 836bccd110..1b022bb03a 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -14,6 +14,7 @@ export const SAVE_ACHIEVEMENTS = 'SAVE_ACHIEVEMENTS'; export const SAVE_GOALS = 'SAVE_GOALS'; export const SAVE_USERS = 'SAVE_USERS'; export const UPDATE_GOAL_PROGRESS = 'UPDATE_GOAL_PROGRESS'; +export const UPDATE_OWN_GOAL_PROGRESS = 'UPDATE_OWN_GOAL_PROGRESS'; export enum AchievementAbility { CORE = 'Core', @@ -57,7 +58,7 @@ export type AchievementItem = { title: string; ability: AchievementAbility; xp: number; - variableXp: boolean; + isVariableXp: boolean; deadline?: Date; release?: Date; isTask: boolean; diff --git a/src/features/game/save/GameSaveRequests.ts b/src/features/game/save/GameSaveRequests.ts index e564612d38..4348ce1977 100644 --- a/src/features/game/save/GameSaveRequests.ts +++ b/src/features/game/save/GameSaveRequests.ts @@ -23,7 +23,7 @@ export async function saveData(fullSaveState: FullSaveState) { }) }; - const resp = await fetch(`${Constants.backendUrl}/v1/user/game_states`, options); + const resp = await fetch(`${Constants.backendUrl}/v2/user/game_states`, options); if (resp && resp.ok) { return resp; @@ -40,7 +40,7 @@ export async function loadData(): Promise { headers: createHeaders(SourceAcademyGame.getInstance().getAccountInfo().accessToken) }; - const resp = await fetch(`${Constants.backendUrl}/v1/user/`, options); + const resp = await fetch(`${Constants.backendUrl}/v2/user/`, options); const message = await resp.text(); const json = JSON.parse(message).gameStates; diff --git a/src/features/storySimulator/StorySimulatorRequest.ts b/src/features/storySimulator/StorySimulatorRequest.ts index 0b67e3a3f5..f715b0286c 100644 --- a/src/features/storySimulator/StorySimulatorRequest.ts +++ b/src/features/storySimulator/StorySimulatorRequest.ts @@ -22,7 +22,7 @@ const sendRequest = (route: string) => async ( ...requestDetails }; - return fetch(Constants.backendUrl + `/v1/${route}/` + requestPath, config); + return fetch(Constants.backendUrl + `/v2/${route}/` + requestPath, config); } finally { } }; From 439e0057bb3606902e73c2c2116254da4aa350b6 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Sun, 18 Apr 2021 13:47:13 +0800 Subject: [PATCH 128/143] Memoised AchievementControl. --- .../achievement/control/AchievementEditor.tsx | 65 ++++++++++--------- .../achievement/control/GoalEditor.tsx | 58 ++++++++++------- .../achievementEditor/EditableCard.tsx | 22 +++++-- .../control/goalEditor/EditableGoal.tsx | 20 ++++-- .../utils/AchievementInferencer.ts | 6 ++ src/commons/sagas/RequestsSaga.ts | 8 +-- src/commons/sagas/WorkspaceSaga.ts | 6 +- 7 files changed, 115 insertions(+), 70 deletions(-) diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index b545b30771..f11c91c591 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -8,6 +8,8 @@ type AchievementEditorProps = { requestPublish: () => void; }; +let editableCards: JSX.Element[] = []; + function AchievementEditor(props: AchievementEditorProps) { const { requestPublish } = props; @@ -23,46 +25,49 @@ function AchievementEditor(props: AchievementEditorProps) { * is being added to the system and the admin is not allowed to add two achievements * at one go. The newUuid holds the newly created achievement uuid until the new achievement * is added into the inferencer. - * - * NOTE: was previously NaN by default, unsure how this should change for uuid */ const [newUuid, setNewUuid] = useState(''); const allowNewUuid = newUuid === ''; - const releaseUuid = (uuid: string) => (uuid === newUuid ? setNewUuid('') : undefined); + const releaseUuid = () => setNewUuid(''); - /** - * Generates components - * - * @param achievementUuids an array of achievementUuid - */ - const generateEditableCards = (achievementUuids: string[]) => - achievementUuids.map(uuid => ( - - )); + const removeCard = (uuid: string) => { + let idx = 0; + while (editableCards[idx].key !== uuid && idx < editableCards.length) { + idx++; + } + editableCards.splice(idx, 1); + } + + const generateEditableCard = (achievementUuid: string, isNewAchievement: boolean) => ( + + ); + + // load preexisting achievements from the inferencer + if (editableCards.length === 0) { + editableCards = inferencer.listSortedAchievementUuids().map(uuid => generateEditableCard(uuid, false)); + } + + const addNewAchievement = (uuid: string) => { + setNewUuid(uuid) + // keep the new achievement on top by swapping it with the first achievement + editableCards[editableCards.length] = editableCards[0]; + editableCards[0] = generateEditableCard(uuid, true); + } - // NOTE: editable cards used to be sorted by id in descending order - // However, UUID removes the guarantee of order preserving IDs return (
    - +
      - {generateEditableCards( - inferencer - .getAllAchievementUuids() - .reverse() - .sort( - (a, b) => - inferencer.getAchievementPositionByUuid(a) - - inferencer.getAchievementPositionByUuid(b) - ) - )} + {editableCards}
    ); diff --git a/src/commons/achievement/control/GoalEditor.tsx b/src/commons/achievement/control/GoalEditor.tsx index cbf9855ef3..14888fbf9d 100644 --- a/src/commons/achievement/control/GoalEditor.tsx +++ b/src/commons/achievement/control/GoalEditor.tsx @@ -8,6 +8,8 @@ type GoalEditorProps = { requestPublish: () => void; }; +let editableGoals: JSX.Element[] = []; + function GoalEditor(props: GoalEditorProps) { const { requestPublish } = props; @@ -23,37 +25,49 @@ function GoalEditor(props: GoalEditorProps) { * is being added to the system and the admin is not allowed to add two goals * at one go. The newUuid holds the newly created goal uuid until the new goal * is added into the inferencer. - * - * NOTE: was previously NaN by default, not sure how this should change for uuid */ const [newUuid, setNewUuid] = useState(''); const allowNewUuid = newUuid === ''; - const releaseUuid = (uuid: string) => (uuid === newUuid ? setNewUuid('') : undefined); + const releaseUuid = () => setNewUuid(''); + + const removeCard = (uuid: string) => { + let idx = 0; + while (editableGoals[idx].key !== uuid && idx < editableGoals.length) { + idx++; + } + editableGoals.splice(idx, 1); + } + + const generateEditableGoal = (goalUuid: string, isNewGoal: boolean) => ( + + ); + + // load preexisting goals from the inferencer + if (editableGoals.length === 0) { + editableGoals = inferencer.getAllGoalUuids().map(uuid => generateEditableGoal(uuid, false)); + } + + const addNewGoal = (uuid: string) => { + setNewUuid(uuid); + // keep the new goal on top by swapping it with the first element + editableGoals[editableGoals.length] = editableGoals[0]; + editableGoals[0] = generateEditableGoal(uuid, true); + } - /** - * Generates components - * - * @param goalUuids an array of goalUuid - */ - const generateEditableGoals = (goalUuids: string[]) => - goalUuids.map(uuid => ( - - )); - - // NOTE: editable cards used to be sorted by id in descending order - // However, UUID removes the guarantee of order preserving IDs return (
    - +
      - {generateEditableGoals(inferencer.getAllGoalUuids().reverse())} + {editableGoals}
    ); diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index f63ee2db41..9104865ad9 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -2,7 +2,7 @@ import { EditableText, NumericInput } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; import { cloneDeep } from 'lodash'; -import React, { useContext, useMemo, useReducer } from 'react'; +import React, { useContext, useMemo, useReducer, useState } from 'react'; import { AchievementContext } from '../../../../features/achievement/AchievementConstants'; import { @@ -24,7 +24,9 @@ import EditableView from './EditableView'; type EditableCardProps = { uuid: string; - releaseUuid: (uuid: string) => void; + isNewAchievement: boolean + releaseUuid: () => void; + removeCard: (uuid: string) => void; requestPublish: () => void; }; @@ -144,20 +146,24 @@ const reducer = (state: State, action: Action) => { }; function EditableCard(props: EditableCardProps) { - const { uuid, releaseUuid, requestPublish } = props; + const { uuid, isNewAchievement, releaseUuid, removeCard, requestPublish } = props; const inferencer = useContext(AchievementContext); const achievement = inferencer.getAchievement(uuid); const achievementClone = useMemo(() => cloneDeep(achievement), [achievement]); const [state, dispatch] = useReducer(reducer, achievementClone, init); + const [isNew, setIsNew] = useState(isNewAchievement); const { editableAchievement, isDirty } = state; const { ability, cardBackground, deadline, release, title, view, xp } = editableAchievement; const saveChanges = () => { dispatch({ type: ActionType.SAVE_CHANGES }); inferencer.modifyAchievement(editableAchievement); - releaseUuid(uuid); + if (isNew) { + releaseUuid(); + setIsNew(false); + } requestPublish(); }; @@ -167,7 +173,11 @@ function EditableCard(props: EditableCardProps) { const deleteAchievement = () => { dispatch({ type: ActionType.DELETE_ACHIEVEMENT }); inferencer.removeAchievement(uuid); - releaseUuid(uuid); + if (isNew) { + releaseUuid(); + setIsNew(false); + } + removeCard(uuid); requestPublish(); }; @@ -185,7 +195,7 @@ function EditableCard(props: EditableCardProps) { // add the current achievement into the goals chosen goalUuids.forEach(goalUuid => { const goal = inferencer.getGoal(goalUuid); - // iterate through the achievements, if the uuid is ot found, add it in + // iterate through the achievements, if the uuid is not found, add it in const len = goal.achievementUuids.length; for (let i = 0; i < len; i++) { if (goal.achievementUuids[i] === uuid) { diff --git a/src/commons/achievement/control/goalEditor/EditableGoal.tsx b/src/commons/achievement/control/goalEditor/EditableGoal.tsx index bcfa578c29..f904637619 100644 --- a/src/commons/achievement/control/goalEditor/EditableGoal.tsx +++ b/src/commons/achievement/control/goalEditor/EditableGoal.tsx @@ -1,6 +1,6 @@ import { EditableText } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; -import React, { useContext, useMemo, useReducer } from 'react'; +import React, { useContext, useMemo, useReducer, useState } from 'react'; import { AchievementContext } from 'src/features/achievement/AchievementConstants'; import { GoalDefinition, GoalMeta } from 'src/features/achievement/AchievementTypes'; @@ -15,7 +15,9 @@ import EditableMeta from './EditableMeta'; type EditableGoalProps = { uuid: string; - releaseUuid: (uuid: string) => void; + isNewGoal: boolean; + releaseUuid: () => void; + removeCard: (uuid: string) => void; requestPublish: () => void; }; @@ -62,20 +64,24 @@ const reducer = (state: State, action: Action) => { }; function EditableGoal(props: EditableGoalProps) { - const { uuid, releaseUuid, requestPublish } = props; + const { uuid, isNewGoal, releaseUuid, removeCard, requestPublish } = props; const inferencer = useContext(AchievementContext); const goal = inferencer.getGoalDefinition(uuid); const goalClone = useMemo(() => cloneDeep(goal), [goal]); const [state, dispatch] = useReducer(reducer, goalClone, init); + const [isNew, setIsNew] = useState(isNewGoal); const { editableGoal, isDirty } = state; const { meta, text } = editableGoal; const saveChanges = () => { dispatch({ type: ActionType.SAVE_CHANGES }); inferencer.modifyGoalDefinition(editableGoal); - releaseUuid(uuid); + if (isNew) { + releaseUuid(); + setIsNew(false); + } requestPublish(); }; @@ -84,7 +90,11 @@ function EditableGoal(props: EditableGoalProps) { const deleteGoal = () => { dispatch({ type: ActionType.DELETE_GOAL }); inferencer.removeGoalDefinition(uuid); - releaseUuid(uuid); + if (isNew) { + releaseUuid(); + setIsNew(false); + } + removeCard(uuid); requestPublish(); }; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index df1ca59ca6..49158d4a8e 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -224,6 +224,12 @@ class AchievementInferencer { public getAchievementsByGoal(goalUuid: string) { return this.getGoal(goalUuid).achievementUuids; } + + public listSortedAchievementUuids() { + return this.getAllAchievements() + .sort((a, b) => a.position - b.position) + .map((achievement => achievement.uuid)); + } /** * Returns true if the goal is invalid diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index e83476fcd6..7535aca61f 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -391,7 +391,7 @@ export const getAssessmentOverviews = async ( * GET /assessments/{assessmentId} */ export const getAssessment = async (id: number, tokens: Tokens): Promise => { - let resp = await request(`assessments/${id}`, 'POST', { + let resp = await request(`assessments/${id}`, 'GET', { ...tokens, shouldAutoLogout: false, shouldRefresh: true @@ -406,7 +406,7 @@ export const getAssessment = async (id: number, tokens: Tokens): Promise => { - const resp = await request(`assessments/question/${id}/submit`, 'POST', { + const resp = await request(`assessments/question/${id}/answer`, 'POST', { ...tokens, body: { answer: `${answer}` }, noHeaderAccept: true, diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index 594e9b196a..c4a4a42471 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -26,7 +26,7 @@ import { EventType } from '../../features/achievement/AchievementTypes'; import ListVisualizer from '../../features/listVisualizer/ListVisualizer'; import { PlaygroundState } from '../../features/playground/PlaygroundTypes'; import { DeviceSession } from '../../features/remoteExecution/RemoteExecutionTypes'; -import { processEvent } from '../achievement/utils/eventHandler'; +import { processEvent } from '../achievement/utils/EventHandler'; import { OverallState, styliseSublanguage } from '../application/ApplicationTypes'; import { externalLibraries, ExternalLibraryName } from '../application/types/ExternalTypes'; import { @@ -739,7 +739,7 @@ export function* evalCode( const typeErrors = parsed && typeCheck(validateAndAnnotate(parsed!, context), context)[1]; context.errors = oldErrors; // for achievement event tracking - const events = context.errors[0] ? [EventType.ERROR] : []; + const events = context.errors.length > 0 ? [EventType.ERROR] : []; // report infinite loops but only for 'vanilla'/default source if (context.variant === 'default') { const infiniteLoopData = getInfiniteLoopData(context, code); @@ -776,7 +776,7 @@ export function* evalCode( // For EVAL_EDITOR and EVAL_REPL, we send notification to workspace that a program has been evaluated if (actionType === EVAL_EDITOR || actionType === EVAL_REPL) { - if (context.errors[0]) { + if (context.errors.length > 0) { processEvent([EventType.ERROR]); } yield put(notifyProgramEvaluated(result, lastDebuggerResult, code, context, workspaceLocation)); From 35c9dc05a6698c322dcabb8592c3b9ddce7f843c Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Sun, 18 Apr 2021 13:50:24 +0800 Subject: [PATCH 129/143] Fixed formatting. --- .../achievement/control/AchievementEditor.tsx | 14 +++++++------- src/commons/achievement/control/GoalEditor.tsx | 10 ++++------ .../control/achievementEditor/EditableCard.tsx | 2 +- .../achievement/utils/AchievementInferencer.ts | 4 ++-- src/commons/sagas/RequestsSaga.ts | 2 +- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/commons/achievement/control/AchievementEditor.tsx b/src/commons/achievement/control/AchievementEditor.tsx index f11c91c591..bef7a6113f 100644 --- a/src/commons/achievement/control/AchievementEditor.tsx +++ b/src/commons/achievement/control/AchievementEditor.tsx @@ -36,7 +36,7 @@ function AchievementEditor(props: AchievementEditorProps) { idx++; } editableCards.splice(idx, 1); - } + }; const generateEditableCard = (achievementUuid: string, isNewAchievement: boolean) => ( generateEditableCard(uuid, false)); + editableCards = inferencer + .listSortedAchievementUuids() + .map(uuid => generateEditableCard(uuid, false)); } const addNewAchievement = (uuid: string) => { - setNewUuid(uuid) + setNewUuid(uuid); // keep the new achievement on top by swapping it with the first achievement editableCards[editableCards.length] = editableCards[0]; editableCards[0] = generateEditableCard(uuid, true); - } + }; return (
    -
      - {editableCards} -
    +
      {editableCards}
    ); } diff --git a/src/commons/achievement/control/GoalEditor.tsx b/src/commons/achievement/control/GoalEditor.tsx index 14888fbf9d..01cc85c779 100644 --- a/src/commons/achievement/control/GoalEditor.tsx +++ b/src/commons/achievement/control/GoalEditor.tsx @@ -29,14 +29,14 @@ function GoalEditor(props: GoalEditorProps) { const [newUuid, setNewUuid] = useState(''); const allowNewUuid = newUuid === ''; const releaseUuid = () => setNewUuid(''); - + const removeCard = (uuid: string) => { let idx = 0; while (editableGoals[idx].key !== uuid && idx < editableGoals.length) { idx++; } editableGoals.splice(idx, 1); - } + }; const generateEditableGoal = (goalUuid: string, isNewGoal: boolean) => (
    -
      - {editableGoals} -
    +
      {editableGoals}
    ); } diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index 9104865ad9..b7842f266c 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -24,7 +24,7 @@ import EditableView from './EditableView'; type EditableCardProps = { uuid: string; - isNewAchievement: boolean + isNewAchievement: boolean; releaseUuid: () => void; removeCard: (uuid: string) => void; requestPublish: () => void; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 49158d4a8e..d02585b702 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -224,11 +224,11 @@ class AchievementInferencer { public getAchievementsByGoal(goalUuid: string) { return this.getGoal(goalUuid).achievementUuids; } - + public listSortedAchievementUuids() { return this.getAllAchievements() .sort((a, b) => a.position - b.position) - .map((achievement => achievement.uuid)); + .map(achievement => achievement.uuid); } /** diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 7535aca61f..7c5f73de77 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -288,7 +288,7 @@ export const editGoal = async ( /** * POST /self/goals/{goalUuid}/progress */ - export const updateOwnGoalProgress = async ( +export const updateOwnGoalProgress = async ( progress: GoalProgress, tokens: Tokens ): Promise => { From 3bb11775a7947d6f0a5b6d8dba48ab1948994d69 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 12:03:06 +0800 Subject: [PATCH 130/143] Shifted event handling into the saga. Handling events now happens every 5 seconds if there are events queued. Also removed redundant dependency between goal and achievement when adding goals to achievements. --- .../achievementEditor/EditableCard.tsx | 12 -- .../achievement/utils/AchievementBackender.ts | 3 +- .../utils/AchievementInferencer.ts | 24 +++ src/commons/achievement/utils/eventHandler.ts | 138 ++++++------------ src/commons/sagas/AchievementSaga.ts | 53 +++++++ .../achievement/AchievementActions.ts | 4 + src/features/achievement/AchievementTypes.ts | 1 + 7 files changed, 127 insertions(+), 108 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/EditableCard.tsx b/src/commons/achievement/control/achievementEditor/EditableCard.tsx index b7842f266c..a494db1c2c 100644 --- a/src/commons/achievement/control/achievementEditor/EditableCard.tsx +++ b/src/commons/achievement/control/achievementEditor/EditableCard.tsx @@ -192,18 +192,6 @@ function EditableCard(props: EditableCardProps) { const changeGoalUuids = (goalUuids: string[]) => { dispatch({ type: ActionType.CHANGE_GOAL_UUIDS, payload: goalUuids }); - // add the current achievement into the goals chosen - goalUuids.forEach(goalUuid => { - const goal = inferencer.getGoal(goalUuid); - // iterate through the achievements, if the uuid is not found, add it in - const len = goal.achievementUuids.length; - for (let i = 0; i < len; i++) { - if (goal.achievementUuids[i] === uuid) { - return; - } - } - goal.achievementUuids[len] = uuid; - }); }; const changePosition = (position: number) => diff --git a/src/commons/achievement/utils/AchievementBackender.ts b/src/commons/achievement/utils/AchievementBackender.ts index 0d63b3ae24..11dd76fcb9 100644 --- a/src/commons/achievement/utils/AchievementBackender.ts +++ b/src/commons/achievement/utils/AchievementBackender.ts @@ -12,8 +12,7 @@ export const backendifyGoalDefinition = (goal: GoalDefinition) => ({ meta: goal.meta, text: goal.text, type: goal.meta.type, - uuid: goal.uuid, - achievementUuids: goal.achievementUuids + uuid: goal.uuid }); export const frontendifyAchievementGoal = (goal: any) => diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index d02585b702..865f5c004e 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -9,6 +9,7 @@ import { AchievementStatus, defaultGoalProgress, GoalDefinition, + GoalProgress, GoalType } from '../../../features/achievement/AchievementTypes'; import { isExpired, isReleased } from './DateHelper'; @@ -185,6 +186,10 @@ class AchievementInferencer { return this.getGoal(uuid) as GoalDefinition; } + public getGoalProgress(uuid: string) { + return this.getGoal(uuid) as GoalProgress; + } + /** * Returns the position of the achievement * @@ -380,6 +385,24 @@ class AchievementInferencer { this.processGoals(); } + public modifyGoalProgress(progress: GoalProgress) { + const goal = this.getGoal(progress.uuid); + if (this.isInvalidGoal(goal)) { + return; + } + this.goalList.set(progress.uuid, { ...goal, ...progress }); + + const achievementUuids = goal.achievementUuids; + for (const uuid in achievementUuids) { + const node = this.nodeList.get(uuid); + if (node) { + this.generateXp(node); + this.generateProgressFrac(node); + this.generateStatus(node); + } + } + } + /** * Removes the AchievementItem from the Inferencer * @@ -686,6 +709,7 @@ class AchievementInferencer { this.goalList.forEach(goal => { const { text, uuid } = goal; this.textToUuid.set(text, uuid); + goal.achievementUuids = uniq(goal.achievementUuids); }); } diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/eventHandler.ts index 70af0ceeb0..78ecc9810f 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/eventHandler.ts @@ -1,10 +1,7 @@ -import { - getAchievements, - getOwnGoals, - updateOwnGoalProgress -} from '../../../features/achievement/AchievementActions'; +import { handleEvent } from '../../../features/achievement/AchievementActions'; import { AchievementGoal, + AchievementItem, EventMeta, EventType, GoalProgress, @@ -26,10 +23,34 @@ function eventShouldCount(meta: EventMeta): boolean { return false; } -let inferencer: AchievementInferencer = new AchievementInferencer([], []); +export function incrementCount(goalUuid: string, inferencer: AchievementInferencer) { + const progress: GoalProgress = { ...inferencer.getGoalProgress(goalUuid) }; + progress.count = progress.count + 1; + const wasCompleted = progress.completed; + progress.completed = progress.count >= progress.targetCount; -function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { - if (goal.meta.type === GoalType.EVENT) { + const incompleteAchievements: string[] = []; + if (!wasCompleted && progress.completed) { + const achievements: string[] = inferencer.getAchievementsByGoal(goalUuid); + for (const achievement of achievements) { + if (!inferencer.isCompleted(inferencer.getAchievement(achievement))) { + incompleteAchievements.push(achievement); + } + } + } + + inferencer.modifyGoalProgress(progress); + + for (const achievementUuid of incompleteAchievements) { + const achievement: AchievementItem = inferencer.getAchievement(achievementUuid); + if (inferencer.isCompleted(achievement)) { + showSuccessMessage('Completed acheivement: ' + achievement.title); + } + } +} + +export function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { + if (goal.meta.type === GoalType.EVENT && eventShouldCount(goal.meta)) { for (let i = 0; i < goal.meta.eventNames.length; i++) { for (let j = 0; j < eventNames.length; j++) { if (goal.meta.eventNames[i] === eventNames[j]) { @@ -37,97 +58,26 @@ function goalIncludesEvents(goal: AchievementGoal, eventNames: EventType[]) { } } } - return false; - } else { - return false; } + return false; } -export function processEvent( - eventNames: EventType[], - increment: number = 1, - retry: boolean = false -) { - // by default, userId should be the current state's one - const userId = store.getState().session.userId; - // just in case userId is still not defined - if (!userId) { - return; - } - - let goals = inferencer.getAllGoals(); - - // if the inferencer has goals, enter the function body - // if this is the retry, also enter the function body to prevent infinite loops - if (goals[0] || retry) { - goals = goals.filter(goal => goalIncludesEvents(goal, eventNames)); - - const computeCompleted = (goal: AchievementGoal): boolean => { - // all goals that are input as arguments are eventGoals - const meta = goal.meta as EventMeta; - - // if the goal just became completed - if (!goal.completed && goal.count + increment >= meta.targetCount) { - goal.completed = true; - const parentAchievements = inferencer.getAchievementsByGoal(goal.uuid); - parentAchievements.forEach(uuid => { - const achievement = inferencer.getAchievement(uuid); - // something went wrong - if (inferencer.isInvalidAchievement(achievement)) { - return; - } - if (inferencer.isCompleted(achievement)) { - showSuccessMessage('Completed acheivement: ' + achievement.title); - } - }); - return true; - } else { - return goal.completed; - } - }; +let timeoutSet: boolean = false; +let loggedEvents: EventType[][] = []; - goals.forEach(goal => { - if (eventShouldCount(goal.meta as EventMeta)) { - // edit the version that is on the state - computeCompleted(goal); - goal.count = goal.count + increment; - - // send the update request to the backend - const progress: GoalProgress = { - uuid: goal.uuid, - count: goal.count, // user gets all of this xp, even if its not complete - targetCount: goal.targetCount, // when complete, the user gets the xp - // check for completion using counter that gets incremented - completed: goal.completed - }; - - // update goal progress in the backend - userId && store.dispatch(updateOwnGoalProgress(progress)); - } - }); - // if goals are not in state, load the goals from the backend and try again - } else { - const retry = () => { - inferencer = new AchievementInferencer( - store.getState().achievement.achievements, - store.getState().achievement.goals - ); - processEvent(eventNames, increment, true); - }; - - if (!store.getState().achievement.goals[0]) { - // ensure that the next function call has updated XP values - store.dispatch(getOwnGoals()); - store.dispatch(getAchievements()); +function resetLoggedEvents() { + loggedEvents = []; + timeoutSet = false; +} - // naively wait for 1 second for the state to be updated - // want: wait exactly until the store does get updated, how? +export function processEvent(eventNames: EventType[]) { + loggedEvents.push(eventNames); - // wait till the getOwnGoals completes and the goals are in the state, then try again - setTimeout(retry, 1000); // arbitrary number of time to wait for the state to get the goals - } else { - // state already has the goals and achievements - retry(); - } + if (!timeoutSet) { + timeoutSet = true; + setTimeout(() => { + store.dispatch(handleEvent(loggedEvents)); + resetLoggedEvents(); + }, 5000); } } diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index c1c214a90b..48f21b048f 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -2,19 +2,24 @@ import { SagaIterator } from 'redux-saga'; import { call, put, select } from 'redux-saga/effects'; import { + AchievementGoal, BULK_UPDATE_ACHIEVEMENTS, BULK_UPDATE_GOALS, EDIT_ACHIEVEMENT, EDIT_GOAL, + EventType, GET_ACHIEVEMENTS, GET_GOALS, GET_OWN_GOALS, GET_USERS, + HANDLE_EVENT, REMOVE_ACHIEVEMENT, REMOVE_GOAL, UPDATE_GOAL_PROGRESS, UPDATE_OWN_GOAL_PROGRESS } from '../../features/achievement/AchievementTypes'; +import AchievementInferencer from '../achievement/utils/AchievementInferencer'; +import { goalIncludesEvents, incrementCount } from '../achievement/utils/EventHandler'; import { OverallState } from '../application/ApplicationTypes'; import { actions } from '../utils/ActionsHelper'; import { @@ -222,4 +227,52 @@ export default function* AchievementSaga(): SagaIterator { } } ); + + yield takeEvery(HANDLE_EVENT, function* (action: ReturnType) { + const tokens = yield select((state: OverallState) => ({ + accessToken: state.session.accessToken, + refreshToken: state.session.refreshToken + })); + + // get the most recent list of achievements + const backendAchievements = yield call(getAchievements, tokens); + if (backendAchievements) { + yield put(actions.saveAchievements(backendAchievements)); + } + + // get the goals and their progress from the backend + const backendGoals = yield call(getOwnGoals, tokens); + if (backendGoals) { + yield put(actions.saveGoals(backendGoals)); + } else { + // if there are no goals, handling events will do nothing + return; + } + + const achievements = yield select((state: OverallState) => state.achievement.achievements); + const goals = yield select((state: OverallState) => state.achievement.goals); + + const inferencer = new AchievementInferencer(achievements, goals); + const loggedEvents: EventType[][] = action.payload; + + const updatedGoals = new Set(); + + loggedEvents.forEach((events: EventType[]) => { + const eventGoalUuids = goals + .filter((goal: AchievementGoal) => goalIncludesEvents(goal, events)) + .map((goal: AchievementGoal) => goal.uuid); + eventGoalUuids.forEach((uuid: string) => { + incrementCount(uuid, inferencer); + updatedGoals.add(uuid); + }); + }); + + for (const uuid of updatedGoals) { + const resp = yield call(updateOwnGoalProgress, inferencer.getGoalProgress(uuid), tokens); + + if (!resp) { + return; + } + } + }); } diff --git a/src/features/achievement/AchievementActions.ts b/src/features/achievement/AchievementActions.ts index bc391c6be4..a1cc2dff59 100644 --- a/src/features/achievement/AchievementActions.ts +++ b/src/features/achievement/AchievementActions.ts @@ -8,12 +8,14 @@ import { BULK_UPDATE_GOALS, EDIT_ACHIEVEMENT, EDIT_GOAL, + EventType, GET_ACHIEVEMENTS, GET_GOALS, GET_OWN_GOALS, GET_USERS, GoalDefinition, GoalProgress, + HANDLE_EVENT, REMOVE_ACHIEVEMENT, REMOVE_GOAL, SAVE_ACHIEVEMENTS, @@ -48,6 +50,8 @@ export const removeGoal = (uuid: string) => action(REMOVE_GOAL, uuid); export const updateOwnGoalProgress = (progress: GoalProgress) => action(UPDATE_OWN_GOAL_PROGRESS, progress); +export const handleEvent = (loggedEvents: EventType[][]) => action(HANDLE_EVENT, loggedEvents); + export const updateGoalProgress = (studentId: number, progress: GoalProgress) => action(UPDATE_GOAL_PROGRESS, { studentId, progress }); diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index 1b022bb03a..c3eba2ecc7 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -4,6 +4,7 @@ export const BULK_UPDATE_ACHIEVEMENTS = 'BULK_UPDATE_ACHIEVEMENTS'; export const BULK_UPDATE_GOALS = 'BULK_UPDATE_GOALS'; export const EDIT_ACHIEVEMENT = 'EDIT_ACHIEVEMENT'; export const EDIT_GOAL = 'EDIT_GOAL'; +export const HANDLE_EVENT = 'HANDLE_EVENT'; export const GET_ACHIEVEMENTS = 'GET_ACHIEVEMENTS'; export const GET_GOALS = 'GET_GOALS'; export const GET_OWN_GOALS = 'GET_OWN_GOALS'; From 0aea64832db788d0f2802687d4c5e00a46952cf4 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:03:13 +0800 Subject: [PATCH 131/143] Removed XP and achievement type goals. --- .../achievement/AchievementManualEditor.tsx | 5 +- .../control/goalEditor/EditableMeta.tsx | 15 +---- .../control/goalEditor/GoalTemplate.ts | 10 ---- .../metaDetails/EditableAchievementsMeta.tsx | 32 ----------- .../goalEditor/metaDetails/EditableXpMeta.tsx | 32 ----------- .../utils/AchievementInferencer.ts | 13 ++--- src/features/achievement/AchievementTypes.ts | 20 +------ .../subcomponents/AchievementDashboard.tsx | 57 +------------------ .../AchievementDashboardContainer.ts | 1 - 9 files changed, 10 insertions(+), 175 deletions(-) delete mode 100644 src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx delete mode 100644 src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx diff --git a/src/commons/achievement/AchievementManualEditor.tsx b/src/commons/achievement/AchievementManualEditor.tsx index a83cd2bbfa..301a055e2e 100644 --- a/src/commons/achievement/AchievementManualEditor.tsx +++ b/src/commons/achievement/AchievementManualEditor.tsx @@ -24,9 +24,8 @@ function AchievementManualEditor(props: AchievementManualEditorProps) { const { studio, getUsers, updateGoalProgress } = props; const users = studio === 'Staff' - ? props.users.sort((user1, user2) => user1.name.localeCompare(user2.name)) - : // Not sure how studio is represented as a string - props.users + ? [...props.users].sort((user1, user2) => user1.name.localeCompare(user2.name)) + : props.users .filter(user => user.group === studio) .sort((user1, user2) => user1.name.localeCompare(user2.name)); diff --git a/src/commons/achievement/control/goalEditor/EditableMeta.tsx b/src/commons/achievement/control/goalEditor/EditableMeta.tsx index 20712e3724..623c591b2c 100644 --- a/src/commons/achievement/control/goalEditor/EditableMeta.tsx +++ b/src/commons/achievement/control/goalEditor/EditableMeta.tsx @@ -3,23 +3,19 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import { ItemRenderer, Select } from '@blueprintjs/select'; import React from 'react'; import { - AchievementsMeta, AssessmentMeta, BinaryMeta, EventMeta, GoalMeta, GoalType, - ManualMeta, - XpMeta + ManualMeta } from 'src/features/achievement/AchievementTypes'; import { metaTemplate } from './GoalTemplate'; -import EditableAchievementsMeta from './metaDetails/EditableAchievementsMeta'; import EditableAssessmentMeta from './metaDetails/EditableAssessmentMeta'; import EditableBinaryMeta from './metaDetails/EditableBinaryMeta'; import EditableEventMeta from './metaDetails/EditableEventMeta'; import EditableManualMeta from './metaDetails/EditableManualMeta'; -import EditableXpMeta from './metaDetails/EditableXpMeta'; type EditableMetaProps = { changeMeta: (meta: GoalMeta) => void; @@ -49,15 +45,6 @@ function EditableMeta(props: EditableMetaProps) { return ; case GoalType.EVENT: return ; - case GoalType.XP: - return ; - case GoalType.ACHIEVEMENTS: - return ( - - ); default: return null; } diff --git a/src/commons/achievement/control/goalEditor/GoalTemplate.ts b/src/commons/achievement/control/goalEditor/GoalTemplate.ts index 9db4200fd3..615ff2a21e 100644 --- a/src/commons/achievement/control/goalEditor/GoalTemplate.ts +++ b/src/commons/achievement/control/goalEditor/GoalTemplate.ts @@ -36,16 +36,6 @@ export const metaTemplate = (type: GoalType): GoalMeta => { observeFrom: undefined, observeTo: undefined }; - case GoalType.XP: - return { - type: GoalType.XP, - targetCount: 100 - }; - case GoalType.ACHIEVEMENTS: - return { - type: GoalType.ACHIEVEMENTS, - targetCount: 1 - }; } }; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx deleted file mode 100644 index 5992bd89e0..0000000000 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableAchievementsMeta.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { NumericInput } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import { AchievementsMeta, GoalMeta } from 'src/features/achievement/AchievementTypes'; - -type EditableAchievementsMetaProps = { - changeMeta: (meta: GoalMeta) => void; - achievementsMeta: AchievementsMeta; -}; - -function EditableAchievementsMeta(props: EditableAchievementsMetaProps) { - const { changeMeta, achievementsMeta } = props; - const { targetCount } = achievementsMeta; - - const changeTargetCount = (targetCount: number) => - changeMeta({ ...achievementsMeta, targetCount: targetCount }); - - return ( - - - - ); -} - -export default EditableAchievementsMeta; diff --git a/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx b/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx deleted file mode 100644 index 960bd52437..0000000000 --- a/src/commons/achievement/control/goalEditor/metaDetails/EditableXpMeta.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { NumericInput } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import { GoalMeta, XpMeta } from 'src/features/achievement/AchievementTypes'; - -type EditableXpMetaProps = { - changeMeta: (meta: GoalMeta) => void; - xpMeta: XpMeta; -}; - -function EditableXpMeta(props: EditableXpMetaProps) { - const { changeMeta, xpMeta } = props; - const { targetCount } = xpMeta; - - const changeTargetCount = (targetCount: number) => - changeMeta({ ...xpMeta, targetCount: targetCount }); - - return ( - - - - ); -} - -export default EditableXpMeta; diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 865f5c004e..8b0f557360 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -420,14 +420,9 @@ class AchievementInferencer { return new AchievementNode(node.achievement); }; - // first, remove the references to this achievement from the goals - this.getAchievement(targetUuid).goalUuids.forEach(goalUuid => { - const goal = this.getGoal(goalUuid); - goal.achievementUuids = goal.achievementUuids.filter(uuid => uuid !== targetUuid); - }); - // then, remove achievement from node list + // first, remove achievement from node list this.nodeList.delete(targetUuid); - // finally, remove reference of the target in other achievement's prerequisite + // then, remove reference of the target in other achievement's prerequisite this.nodeList.forEach((node, uuid) => { if (hasTarget(node)) { this.nodeList.set(uuid, sanitizeNode(node)); @@ -721,7 +716,7 @@ class AchievementInferencer { private constructNodeList(achievements: AchievementItem[]) { const nodeList = new Map(); achievements.forEach(achievement => - nodeList.set(achievement.uuid, new AchievementNode(achievement)) + nodeList.set(achievement.uuid, new AchievementNode({ ...achievement })) ); return nodeList; } @@ -733,7 +728,7 @@ class AchievementInferencer { */ private constructGoalList(goals: AchievementGoal[]) { const goalList = new Map(); - goals.forEach(goal => goalList.set(goal.uuid, goal)); + goals.forEach(goal => goalList.set(goal.uuid, { ...goal })); return goalList; } diff --git a/src/features/achievement/AchievementTypes.ts b/src/features/achievement/AchievementTypes.ts index c3eba2ecc7..df6cf284dc 100644 --- a/src/features/achievement/AchievementTypes.ts +++ b/src/features/achievement/AchievementTypes.ts @@ -111,8 +111,6 @@ export const defaultGoalProgress = { export enum GoalType { MANUAL = 'Manual', EVENT = 'Event', - XP = 'XP', - ACHIEVEMENTS = 'Achievements', ASSESSMENT = 'Assessment (unsupported)', BINARY = 'Binary (unsupported)' } @@ -125,13 +123,7 @@ export enum EventType { RUN_TESTCASE = 'Run Testcase' } -export type GoalMeta = - | AssessmentMeta - | BinaryMeta - | ManualMeta - | EventMeta - | XpMeta - | AchievementsMeta; +export type GoalMeta = AssessmentMeta | BinaryMeta | ManualMeta | EventMeta; export type AssessmentMeta = { type: GoalType.ASSESSMENT; @@ -160,16 +152,6 @@ export type EventMeta = { observeTo?: Date; }; -export type XpMeta = { - type: GoalType.XP; - targetCount: number; -}; - -export type AchievementsMeta = { - type: GoalType.ACHIEVEMENTS; - targetCount: number; -}; - /** * Information of an achievement view * diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 105000ece7..7137661a39 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -1,5 +1,5 @@ import { IconNames } from '@blueprintjs/icons'; -import { useEffect, useReducer, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Role } from 'src/commons/application/ApplicationTypes'; import { AssessmentOverview } from 'src/commons/assessment/AssessmentTypes'; @@ -11,13 +11,11 @@ import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import insertFakeAchievements from '../../../commons/achievement/utils/InsertFakeAchievements'; import Constants from '../../../commons/utils/Constants'; -import { showSuccessMessage } from '../../../commons/utils/NotificationsHelper'; import { AchievementContext } from '../../../features/achievement/AchievementConstants'; import { AchievementUser, FilterStatus, - GoalProgress, - GoalType + GoalProgress } from '../../../features/achievement/AchievementTypes'; export type DispatchProps = { @@ -66,7 +64,6 @@ function Dashboard(props: DispatchProps & StateProps) { getUsers, updateGoalProgress, fetchAssessmentOverviews, - id, group, inferencer, name, @@ -85,8 +82,6 @@ function Dashboard(props: DispatchProps & StateProps) { } }, [getAchievements, getOwnGoals]); - const [, forceUpdate] = useReducer(x => x + 1, 0); - if (name && role && !assessmentOverviews) { // If assessment overviews are not loaded, fetch them fetchAssessmentOverviews(); @@ -107,54 +102,6 @@ function Dashboard(props: DispatchProps & StateProps) { const focusState = useState(''); const [focusUuid] = focusState; - const completedCount = inferencer.getAllCompletedAchievements().length; - const xp = inferencer.getTotalXp(); - const goals = inferencer.getAllGoals(); - - // Computes goal progress for achievement and xp goals - let changed = false; - goals.forEach(goal => { - if (!id) { - return; - } - let update = false; - if (goal.meta.type === GoalType.ACHIEVEMENTS) { - update = goal.count !== completedCount; - goal.count = completedCount; - } - if (goal.meta.type === GoalType.XP) { - update = goal.count !== xp; - goal.count = xp; - } - if (!goal.completed && goal.count >= goal.targetCount) { - goal.completed = true; - const parentAchievements = inferencer.getAchievementsByGoal(goal.uuid); - parentAchievements.forEach(uuid => { - const achievement = inferencer.getAchievement(uuid); - console.log(achievement); - if (inferencer.isInvalidAchievement(achievement)) { - return; - } - if (inferencer.isCompleted(achievement)) { - showSuccessMessage('Completed acheivement: ' + achievement.title); - } - }); - } - changed = changed || update; - if (update) { - const progress: GoalProgress = { - uuid: goal.uuid, - count: goal.count, - targetCount: goal.targetCount, - completed: goal.completed - }; - updateGoalProgress(id, progress); - } - }); - if (changed) { - setTimeout(forceUpdate, 1000); - } - return (
    diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index 05c9ad9dc7..ca6da2ce7f 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -19,7 +19,6 @@ const mapStateToProps: MapStateToProps = state => inferencer: Constants.useAchievementBackend ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) : new AchievementInferencer(mockAchievements, mockGoals), - id: state.session.userId, name: state.session.name, role: state.session.role, assessmentOverviews: state.session.assessmentOverviews, From 69f3fa2111bb3553b5550d798626a639fc324795 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:10:57 +0800 Subject: [PATCH 132/143] Made inferencer constructor create deep clones. Any changes done in the inferencer will not affect the copy in the overall state. --- src/commons/achievement/utils/AchievementInferencer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index 8b0f557360..d2057ecb64 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -1,4 +1,4 @@ -import { uniq } from 'lodash'; +import { uniq, cloneDeep } from 'lodash'; import { v4 } from 'uuid'; import { showDangerMessage } from '../../../commons/utils/NotificationsHelper'; @@ -716,7 +716,7 @@ class AchievementInferencer { private constructNodeList(achievements: AchievementItem[]) { const nodeList = new Map(); achievements.forEach(achievement => - nodeList.set(achievement.uuid, new AchievementNode({ ...achievement })) + nodeList.set(achievement.uuid, new AchievementNode(cloneDeep(achievement))) ); return nodeList; } @@ -728,7 +728,7 @@ class AchievementInferencer { */ private constructGoalList(goals: AchievementGoal[]) { const goalList = new Map(); - goals.forEach(goal => goalList.set(goal.uuid, { ...goal })); + goals.forEach(goal => goalList.set(goal.uuid, cloneDeep(goal))); return goalList; } From 938e8376e0b2e08a55d81e5d8bed8f033c905ae7 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:14:25 +0800 Subject: [PATCH 133/143] Fixed formatting. --- src/commons/achievement/utils/AchievementInferencer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/achievement/utils/AchievementInferencer.ts b/src/commons/achievement/utils/AchievementInferencer.ts index d2057ecb64..78a87ca0ea 100644 --- a/src/commons/achievement/utils/AchievementInferencer.ts +++ b/src/commons/achievement/utils/AchievementInferencer.ts @@ -1,4 +1,4 @@ -import { uniq, cloneDeep } from 'lodash'; +import { cloneDeep, uniq } from 'lodash'; import { v4 } from 'uuid'; import { showDangerMessage } from '../../../commons/utils/NotificationsHelper'; From 6e3b87d55ba631997deef04682e5ecacfa4d9b3e Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:30:58 +0800 Subject: [PATCH 134/143] Fixed tests related to equality of inferencer. Inferencer now makes a deep copy of every achievement/goal it is constructed with so as to not touch the overall state. --- .../__tests__/AchievementInferencer.test.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index f7b68428f9..8ea9942a6f 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -88,13 +88,13 @@ describe('Achievement Inferencer Constructor', () => { }); describe('Overlapping IDs', () => { - const testAchievement1: AchievementItem = { ...testAchievement, uuid: '1' }; - const testAchievement2: AchievementItem = { ...testAchievement, uuid: '2' }; - const testAchievement3: AchievementItem = { ...testAchievement, uuid: '2' }; + const testAchievement1: AchievementItem = { ...testAchievement, uuid: '1', title: 'testAchievement1' }; + const testAchievement2: AchievementItem = { ...testAchievement, uuid: '2', title: 'testAchievement2' }; + const testAchievement3: AchievementItem = { ...testAchievement, uuid: '2', title: 'testAchievement3' }; - const testGoal1: AchievementGoal = { ...testGoal, uuid: '1' }; - const testGoal2: AchievementGoal = { ...testGoal, uuid: '1' }; - const testGoal3: AchievementGoal = { ...testGoal, uuid: '2' }; + const testGoal1: AchievementGoal = { ...testGoal, uuid: '1', text: 'testGoal1' }; + const testGoal2: AchievementGoal = { ...testGoal, uuid: '1', text: 'testGoal2' }; + const testGoal3: AchievementGoal = { ...testGoal, uuid: '2', text: 'testGoal3' }; const inferencer = new AchievementInferencer( [testAchievement1, testAchievement2, testAchievement3], @@ -107,15 +107,15 @@ describe('Achievement Inferencer Constructor', () => { }); test('References the correct achievements and goals', () => { - expect(inferencer.getAchievement('1')).toBe(testAchievement1); - expect(inferencer.getAchievement('2')).not.toBe(testAchievement2); - expect(inferencer.getAchievement('2')).toBe(testAchievement3); - expect(inferencer.getGoal('1')).not.toBe(testGoal1); - expect(inferencer.getGoal('1')).toBe(testGoal2); - expect(inferencer.getGoal('2')).toBe(testGoal3); - expect(inferencer.getGoalDefinition('1')).not.toBe(testGoal1); - expect(inferencer.getGoalDefinition('1')).toBe(testGoal2); - expect(inferencer.getGoalDefinition('2')).toBe(testGoal3); + expect(inferencer.getAchievement('1')).toEqual(testAchievement1); + expect(inferencer.getAchievement('2')).not.toEqual(testAchievement2); + expect(inferencer.getAchievement('2')).toEqual(testAchievement3); + expect(inferencer.getGoal('1')).not.toEqual(testGoal1); + expect(inferencer.getGoal('1')).toEqual(testGoal2); + expect(inferencer.getGoal('2')).toEqual(testGoal3); + expect(inferencer.getGoalDefinition('1')).not.toEqual(testGoal1); + expect(inferencer.getGoalDefinition('1')).toEqual(testGoal2); + expect(inferencer.getGoalDefinition('2')).toEqual(testGoal3); }); }); }); @@ -146,8 +146,8 @@ describe('Achievement Setter', () => { // After insertion expect(inferencer.getAllAchievements().length).toBe(mockAchievements.length + 1); - expect(inferencer.getAchievement('1')).toBe(mockAchievements[1]); - expect(inferencer.getAchievement(newUuid)).toBe(testAchievement1); + expect(inferencer.getAchievement('1')).toEqual(mockAchievements[1]); + expect(inferencer.getAchievement(newUuid)).toEqual(testAchievement1); expect(inferencer.getTitleByUuid(newUuid)).toBe('Test Achievement 1'); expect(inferencer.getUuidByTitle('Test Achievement 1')).toBe(newUuid); expect(inferencer.getAchievement(newUuid).prerequisiteUuids).toEqual(['2', '3', '4']); @@ -163,7 +163,7 @@ describe('Achievement Setter', () => { // After insertion expect(inferencer.getAllGoals().length).toBe(mockGoals.length + 1); - expect(inferencer.getGoalDefinition('1')).toBe(mockGoals[1]); + expect(inferencer.getGoalDefinition('1')).toEqual(mockGoals[1]); expect(inferencer.getGoalDefinition(newUuid)).toEqual(testGoal1); expect(inferencer.getTextByUuid(newUuid)).toBe('Test Goal 1'); expect(inferencer.getUuidByText('Test Goal 1')).toBe(newUuid); @@ -178,14 +178,14 @@ describe('Achievement Setter', () => { }; // Before modification - expect(inferencer.getAchievement(newUuid)).toBe(testAchievement1); + expect(inferencer.getAchievement(newUuid)).toEqual(testAchievement1); expect(inferencer.getTitleByUuid(newUuid)).toBe('Test Achievement 1'); expect(inferencer.getUuidByTitle('Test Achievement 1')).toBe(newUuid); inferencer.modifyAchievement(testAchievement2); // After modification - expect(inferencer.getAchievement(newUuid)).toBe(testAchievement2); + expect(inferencer.getAchievement(newUuid)).toEqual(testAchievement2); expect(inferencer.getTitleByUuid(newUuid)).toBe('Test Achievement 2'); expect(inferencer.getUuidByTitle('Test Achievement 2')).toBe(newUuid); }); @@ -326,8 +326,8 @@ describe('Achievement Inferencer Getter', () => { ); expect(inferencer.listGoals('1').length).toBe(2); - expect(inferencer.listGoals('1')[0]).toBe(testGoal2); - expect(inferencer.listGoals('1')[1]).toBe(testGoal1); + expect(inferencer.listGoals('1')[0]).toEqual(testGoal2); + expect(inferencer.listGoals('1')[1]).toEqual(testGoal1); expect(inferencer.listGoals('2')).toEqual([]); }); @@ -352,8 +352,8 @@ describe('Achievement Inferencer Getter', () => { ); expect(inferencer.listPrerequisiteGoals('1').length).toBe(2); - expect(inferencer.listPrerequisiteGoals('1')[0]).toBe(testGoal2); - expect(inferencer.listPrerequisiteGoals('1')[1]).toBe(testGoal1); + expect(inferencer.listPrerequisiteGoals('1')[0]).toEqual(testGoal2); + expect(inferencer.listPrerequisiteGoals('1')[1]).toEqual(testGoal1); }); }); From 0d7ebb696debbe8a5ea0a967e4830406dcb7ec86 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:41:42 +0800 Subject: [PATCH 135/143] Fixed formatting. --- .../__tests__/AchievementInferencer.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts index 8ea9942a6f..934de898c6 100644 --- a/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts +++ b/src/commons/achievement/utils/__tests__/AchievementInferencer.test.ts @@ -88,9 +88,21 @@ describe('Achievement Inferencer Constructor', () => { }); describe('Overlapping IDs', () => { - const testAchievement1: AchievementItem = { ...testAchievement, uuid: '1', title: 'testAchievement1' }; - const testAchievement2: AchievementItem = { ...testAchievement, uuid: '2', title: 'testAchievement2' }; - const testAchievement3: AchievementItem = { ...testAchievement, uuid: '2', title: 'testAchievement3' }; + const testAchievement1: AchievementItem = { + ...testAchievement, + uuid: '1', + title: 'testAchievement1' + }; + const testAchievement2: AchievementItem = { + ...testAchievement, + uuid: '2', + title: 'testAchievement2' + }; + const testAchievement3: AchievementItem = { + ...testAchievement, + uuid: '2', + title: 'testAchievement3' + }; const testGoal1: AchievementGoal = { ...testGoal, uuid: '1', text: 'testGoal1' }; const testGoal2: AchievementGoal = { ...testGoal, uuid: '1', text: 'testGoal2' }; From b58efc9f833abda9283d3e68936b950b7ba8d0d7 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:50:27 +0800 Subject: [PATCH 136/143] Renamed eventHandler to EventHandler. --- .../achievement/utils/{eventHandler.ts => EventHandler.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/commons/achievement/utils/{eventHandler.ts => EventHandler.ts} (99%) diff --git a/src/commons/achievement/utils/eventHandler.ts b/src/commons/achievement/utils/EventHandler.ts similarity index 99% rename from src/commons/achievement/utils/eventHandler.ts rename to src/commons/achievement/utils/EventHandler.ts index 78ecc9810f..0dc72be61b 100644 --- a/src/commons/achievement/utils/eventHandler.ts +++ b/src/commons/achievement/utils/EventHandler.ts @@ -22,7 +22,7 @@ function eventShouldCount(meta: EventMeta): boolean { } return false; } - +a export function incrementCount(goalUuid: string, inferencer: AchievementInferencer) { const progress: GoalProgress = { ...inferencer.getGoalProgress(goalUuid) }; progress.count = progress.count + 1; From 1852d7d7af281302a63f3fe6e357b01ecf6a5516 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 19 Apr 2021 13:51:51 +0800 Subject: [PATCH 137/143] Fixed typo. --- src/commons/achievement/utils/EventHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/achievement/utils/EventHandler.ts b/src/commons/achievement/utils/EventHandler.ts index 0dc72be61b..78ecc9810f 100644 --- a/src/commons/achievement/utils/EventHandler.ts +++ b/src/commons/achievement/utils/EventHandler.ts @@ -22,7 +22,7 @@ function eventShouldCount(meta: EventMeta): boolean { } return false; } -a + export function incrementCount(goalUuid: string, inferencer: AchievementInferencer) { const progress: GoalProgress = { ...inferencer.getGoalProgress(goalUuid) }; progress.count = progress.count + 1; From 70b2378b5c945bc4ddc2e9899040f6c6024bf15c Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 20 May 2021 16:15:39 +0800 Subject: [PATCH 138/143] Shorten event delay, remove constant for mock achievements. --- src/commons/achievement/utils/EventHandler.ts | 5 ++++- src/commons/utils/Constants.ts | 2 -- src/pages/achievement/control/AchievementControl.tsx | 3 --- .../achievement/control/AchievementControlContainer.ts | 6 +----- .../achievement/subcomponents/AchievementDashboard.tsx | 3 --- .../subcomponents/AchievementDashboardContainer.ts | 6 +----- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/commons/achievement/utils/EventHandler.ts b/src/commons/achievement/utils/EventHandler.ts index 78ecc9810f..176a3d6cd0 100644 --- a/src/commons/achievement/utils/EventHandler.ts +++ b/src/commons/achievement/utils/EventHandler.ts @@ -12,6 +12,9 @@ import { showSuccessMessage } from '../../utils/NotificationsHelper'; import AchievementInferencer from './AchievementInferencer'; import { isExpired, isReleased, isWithinTimeRange } from './DateHelper'; +// How long the system should wait before sending a request to the backend +const updateInterval = 3000; + function eventShouldCount(meta: EventMeta): boolean { // goal is not released, or has expired if (isExpired(meta.deadline) || !isReleased(meta.release)) { @@ -78,6 +81,6 @@ export function processEvent(eventNames: EventType[]) { setTimeout(() => { store.dispatch(handleEvent(loggedEvents)); resetLoggedEvents(); - }, 5000); + }, updateInterval); } } diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts index 850b4a59dc..8be1da8644 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -12,7 +12,6 @@ const backendUrl = process.env.REACT_APP_BACKEND_URL; const cadetLoggerUrl = isTest ? undefined : process.env.REACT_APP_CADET_LOGGER; const cadetLoggerInterval = parseInt(process.env.REACT_APP_CADET_LOGGER_INTERVAL || '10000', 10); const useBackend = !isTest && isTrue(process.env.REACT_APP_USE_BACKEND); -const useAchievementBackend = !isTest && isTrue(process.env.REACT_APP_USE_ACHIEVEMENT_BACKEND); const defaultSourceChapter = 4; const defaultSourceVariant = 'default'; const defaultQuestionId = 0; @@ -105,7 +104,6 @@ const Constants = { backendUrl, cadetLoggerUrl, useBackend, - useAchievementBackend, defaultSourceChapter, defaultSourceVariant, defaultQuestionId, diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 2241ab065e..318178ddd3 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -5,7 +5,6 @@ import AchievementEditor from '../../../commons/achievement/control/AchievementE import AchievementPreview from '../../../commons/achievement/control/AchievementPreview'; import GoalEditor from '../../../commons/achievement/control/GoalEditor'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; -import Constants from '../../../commons/utils/Constants'; import { AchievementContext } from '../../../features/achievement/AchievementConstants'; import { AchievementItem, GoalDefinition } from '../../../features/achievement/AchievementTypes'; @@ -37,10 +36,8 @@ function AchievementControl(props: DispatchProps & StateProps) { * Fetch the latest achievements and goals from backend when the page is rendered */ useEffect(() => { - if (Constants.useAchievementBackend) { getAchievements(); getOwnGoals(); - } }, [getAchievements, getOwnGoals]); /** diff --git a/src/pages/achievement/control/AchievementControlContainer.ts b/src/pages/achievement/control/AchievementControlContainer.ts index 4c0ea839b4..10ee15c2e3 100644 --- a/src/pages/achievement/control/AchievementControlContainer.ts +++ b/src/pages/achievement/control/AchievementControlContainer.ts @@ -1,10 +1,8 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import Constants from 'src/commons/utils/Constants'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import { OverallState } from '../../../commons/application/ApplicationTypes'; -import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; import { bulkUpdateAchievements, bulkUpdateGoals, @@ -16,9 +14,7 @@ import { import AchievementControl, { DispatchProps, StateProps } from './AchievementControl'; const mapStateToProps: MapStateToProps = state => ({ - inferencer: Constants.useAchievementBackend - ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) - : new AchievementInferencer(mockAchievements, mockGoals) + inferencer: new AchievementInferencer(state.achievement.achievements, state.achievement.goals) }); const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 7137661a39..2b84e03e7a 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -10,7 +10,6 @@ import AchievementTask from '../../../commons/achievement/AchievementTask'; import AchievementView from '../../../commons/achievement/AchievementView'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import insertFakeAchievements from '../../../commons/achievement/utils/InsertFakeAchievements'; -import Constants from '../../../commons/utils/Constants'; import { AchievementContext } from '../../../features/achievement/AchievementConstants'; import { AchievementUser, @@ -76,10 +75,8 @@ function Dashboard(props: DispatchProps & StateProps) { * Fetch the latest achievements and goals from backend when the page is rendered */ useEffect(() => { - if (Constants.useAchievementBackend) { getOwnGoals(); getAchievements(); - } }, [getAchievements, getOwnGoals]); if (name && role && !assessmentOverviews) { diff --git a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts index ca6da2ce7f..b506433e65 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts +++ b/src/pages/achievement/subcomponents/AchievementDashboardContainer.ts @@ -4,8 +4,6 @@ import { bindActionCreators, Dispatch } from 'redux'; import AchievementInferencer from '../../../commons/achievement/utils/AchievementInferencer'; import { fetchAssessmentOverviews } from '../../../commons/application/actions/SessionActions'; import { OverallState } from '../../../commons/application/ApplicationTypes'; -import { mockAchievements, mockGoals } from '../../../commons/mocks/AchievementMocks'; -import Constants from '../../../commons/utils/Constants'; import { getAchievements, getOwnGoals, @@ -16,9 +14,7 @@ import Dashboard, { DispatchProps, StateProps } from './AchievementDashboard'; const mapStateToProps: MapStateToProps = state => ({ group: state.session.group, - inferencer: Constants.useAchievementBackend - ? new AchievementInferencer(state.achievement.achievements, state.achievement.goals) - : new AchievementInferencer(mockAchievements, mockGoals), + inferencer: new AchievementInferencer(state.achievement.achievements, state.achievement.goals), name: state.session.name, role: state.session.role, assessmentOverviews: state.session.assessmentOverviews, From c0f1867c388566a250404f51ede54df20df1197b Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 20 May 2021 16:17:16 +0800 Subject: [PATCH 139/143] Fixed formatting. --- src/pages/achievement/control/AchievementControl.tsx | 4 ++-- src/pages/achievement/subcomponents/AchievementDashboard.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/achievement/control/AchievementControl.tsx b/src/pages/achievement/control/AchievementControl.tsx index 318178ddd3..773a053cc9 100644 --- a/src/pages/achievement/control/AchievementControl.tsx +++ b/src/pages/achievement/control/AchievementControl.tsx @@ -36,8 +36,8 @@ function AchievementControl(props: DispatchProps & StateProps) { * Fetch the latest achievements and goals from backend when the page is rendered */ useEffect(() => { - getAchievements(); - getOwnGoals(); + getAchievements(); + getOwnGoals(); }, [getAchievements, getOwnGoals]); /** diff --git a/src/pages/achievement/subcomponents/AchievementDashboard.tsx b/src/pages/achievement/subcomponents/AchievementDashboard.tsx index 2b84e03e7a..cf73b7efd3 100644 --- a/src/pages/achievement/subcomponents/AchievementDashboard.tsx +++ b/src/pages/achievement/subcomponents/AchievementDashboard.tsx @@ -75,8 +75,8 @@ function Dashboard(props: DispatchProps & StateProps) { * Fetch the latest achievements and goals from backend when the page is rendered */ useEffect(() => { - getOwnGoals(); - getAchievements(); + getOwnGoals(); + getAchievements(); }, [getAchievements, getOwnGoals]); if (name && role && !assessmentOverviews) { From 3c73cf63ee9a3298ceecc1604274e2cdbd5b69ff Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 20 May 2021 17:02:53 +0800 Subject: [PATCH 140/143] Fixed formatting. --- .../achievementEditor/AchievementSettings.tsx | 10 +- src/commons/assessment/Assessment.tsx | 6 +- src/commons/documentation/Documentation.ts | 5 +- .../EditingWorkspaceSideContentHelper.ts | 28 ++--- ...eContentProgrammingQuestionTemplateTab.tsx | 62 +++++----- src/commons/editor/Editor.tsx | 56 ++++----- .../mobileWorkspace/MobileWorkspace.tsx | 5 +- .../__tests__/NotificationBadgeHelper.ts | 5 +- .../SideContentContestEntryCard.tsx | 79 ++++++------ .../SideContentContestLeaderboard.tsx | 87 ++++++------- .../SideContentContestVotingContainer.tsx | 115 +++++++++--------- .../SideContentLeaderboardCard.tsx | 37 +++--- src/commons/utils/Constants.ts | 6 +- src/commons/utils/Hooks.ts | 14 +-- src/features/eventLogging/index.ts | 52 ++++---- src/features/game/action/GameActionManager.ts | 9 +- .../game/dialogue/GameDialogueManager.ts | 8 +- src/features/game/parser/ActionParser.ts | 7 +- src/features/game/parser/ParserValidator.ts | 10 +- .../storySimulator/StorySimulatorRequest.ts | 44 +++---- src/pages/academy/Academy.tsx | 6 +- .../grading/subcomponents/GradingEditor.tsx | 56 +++++---- .../subcomponents/GroundControlDropzone.tsx | 18 +-- 23 files changed, 356 insertions(+), 369 deletions(-) diff --git a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx index a2d90b66e6..7589242e17 100644 --- a/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx +++ b/src/commons/achievement/control/achievementEditor/AchievementSettings.tsx @@ -26,14 +26,8 @@ function AchievementSettings(props: AchievementSettingsProps) { changeIsVariableXp, editableAchievement } = props; - const { - uuid, - cardBackground, - goalUuids, - position, - prerequisiteUuids, - isVariableXp - } = editableAchievement; + const { uuid, cardBackground, goalUuids, position, prerequisiteUuids, isVariableXp } = + editableAchievement; const [isOpen, setOpen] = useState(false); const toggleOpen = () => setOpen(!isOpen); diff --git a/src/commons/assessment/Assessment.tsx b/src/commons/assessment/Assessment.tsx index 42c46050c3..4444c79c78 100644 --- a/src/commons/assessment/Assessment.tsx +++ b/src/commons/assessment/Assessment.tsx @@ -274,9 +274,9 @@ const Assessment: React.FC = props => { const isOverviewUpcoming = (overview: AssessmentOverview) => !beforeNow(overview.closeAt) && !beforeNow(overview.openAt); - const upcomingCards = sortAssessments( - assessmentOverviews.filter(isOverviewUpcoming) - ).map((overview, index) => makeOverviewCard(overview, index, !isStudent, false)); + const upcomingCards = sortAssessments(assessmentOverviews.filter(isOverviewUpcoming)).map( + (overview, index) => makeOverviewCard(overview, index, !isStudent, false) + ); /** Opened assessments, that are released and can be attempted. */ const isOverviewOpened = (overview: AssessmentOverview) => diff --git a/src/commons/documentation/Documentation.ts b/src/commons/documentation/Documentation.ts index 4e2dc987ae..25b1f332bf 100644 --- a/src/commons/documentation/Documentation.ts +++ b/src/commons/documentation/Documentation.ts @@ -38,9 +38,8 @@ for (const [lib, names] of externalLibraries) { // Add remote device libraries for (const deviceType of deviceTypes) { - externalLibrariesDocumentation[deviceType.deviceLibraryName] = deviceType.internalFunctions.map( - mapExternalLibraryName - ); + externalLibrariesDocumentation[deviceType.deviceLibraryName] = + deviceType.internalFunctions.map(mapExternalLibraryName); } const builtinDocumentation = {}; diff --git a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentHelper.ts b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentHelper.ts index be1c0d1af4..a18215a4a7 100644 --- a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentHelper.ts +++ b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentHelper.ts @@ -13,17 +13,17 @@ export const assignToPath = (path: Array, value: any, obj: any) obj[path[i]] = value; }; -export const limitNumberRange = (min: number | null = 0, max: number | null = null) => ( - value: number | string -): number => { - value = typeof value === 'string' ? parseInt(value, 10) : value; - let result; - if (min !== null && value < min) { - result = min; - } else if (max !== null && value > max) { - result = max; - } else { - result = value; - } - return result; -}; +export const limitNumberRange = + (min: number | null = 0, max: number | null = null) => + (value: number | string): number => { + value = typeof value === 'string' ? parseInt(value, 10) : value; + let result; + if (min !== null && value < min) { + result = min; + } else if (max !== null && value > max) { + result = max; + } else { + result = value; + } + return result; + }; diff --git a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentProgrammingQuestionTemplateTab.tsx b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentProgrammingQuestionTemplateTab.tsx index 06ccc88a1c..1757aa49ea 100644 --- a/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentProgrammingQuestionTemplateTab.tsx +++ b/src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentProgrammingQuestionTemplateTab.tsx @@ -183,38 +183,42 @@ export class ProgrammingQuestionTemplateTab extends React.Component) => (e: any): void => { - if (!this.state.templateFocused) { - this.setState({ - templateValue: getValueFromPath(path, this.props.assessment), - templateFocused: true - }); - } - }; - - private unFocusEditor = (path: Array) => (e: any): void => { - if (this.state.templateFocused) { - const value = getValueFromPath(path, this.props.assessment); - if (value !== this.state.templateValue) { - const assessmentVal = this.props.assessment; - assignToPath(path, this.state.templateValue, assessmentVal); - this.props.updateAssessment(assessmentVal); + private focusEditor = + (path: Array) => + (e: any): void => { + if (!this.state.templateFocused) { + this.setState({ + templateValue: getValueFromPath(path, this.props.assessment), + templateFocused: true + }); } + }; - if (this.state.activeEditor.id === 'prepend') { - const editorPrepend = this.state.templateValue; - this.props.handleUpdateWorkspace({ editorPrepend }); - } else if (this.state.activeEditor.id === 'postpend') { - const editorPostpend = this.state.templateValue; - this.props.handleUpdateWorkspace({ editorPostpend }); + private unFocusEditor = + (path: Array) => + (e: any): void => { + if (this.state.templateFocused) { + const value = getValueFromPath(path, this.props.assessment); + if (value !== this.state.templateValue) { + const assessmentVal = this.props.assessment; + assignToPath(path, this.state.templateValue, assessmentVal); + this.props.updateAssessment(assessmentVal); + } + + if (this.state.activeEditor.id === 'prepend') { + const editorPrepend = this.state.templateValue; + this.props.handleUpdateWorkspace({ editorPrepend }); + } else if (this.state.activeEditor.id === 'postpend') { + const editorPostpend = this.state.templateValue; + this.props.handleUpdateWorkspace({ editorPostpend }); + } + + this.setState({ + templateValue: '', + templateFocused: false + }); } - - this.setState({ - templateValue: '', - templateFocused: false - }); - } - }; + }; private handleCopyFromEditor = (path: Array) => (): void => { const assessment = this.props.assessment; diff --git a/src/commons/editor/Editor.tsx b/src/commons/editor/Editor.tsx index 94cf1316e7..07aa2f50e5 100644 --- a/src/commons/editor/Editor.tsx +++ b/src/commons/editor/Editor.tsx @@ -112,35 +112,35 @@ const getMarkers = ( })); }; -const makeHandleGutterClick = ( - handleEditorUpdateBreakpoints: DispatchProps['handleEditorUpdateBreakpoints'] -) => (e: AceMouseEvent) => { - const target = e.domEvent.target! as HTMLDivElement; - if ( - target.className.indexOf('ace_gutter-cell') === -1 || - !e.editor.isFocused() || - e.clientX > 35 + target.getBoundingClientRect().left - ) { - return; - } +const makeHandleGutterClick = + (handleEditorUpdateBreakpoints: DispatchProps['handleEditorUpdateBreakpoints']) => + (e: AceMouseEvent) => { + const target = e.domEvent.target! as HTMLDivElement; + if ( + target.className.indexOf('ace_gutter-cell') === -1 || + !e.editor.isFocused() || + e.clientX > 35 + target.getBoundingClientRect().left + ) { + return; + } - // Breakpoint related. - const row = e.getDocumentPosition().row; - const content = e.editor.session.getLine(row); - const breakpoints = e.editor.session.getBreakpoints(); - if ( - breakpoints[row] === undefined && - content.length !== 0 && - !content.includes('//') && - !content.includes('debugger;') - ) { - e.editor.session.setBreakpoint(row, undefined!); - } else { - e.editor.session.clearBreakpoint(row); - } - e.stop(); - handleEditorUpdateBreakpoints(e.editor.session.getBreakpoints()); -}; + // Breakpoint related. + const row = e.getDocumentPosition().row; + const content = e.editor.session.getLine(row); + const breakpoints = e.editor.session.getBreakpoints(); + if ( + breakpoints[row] === undefined && + content.length !== 0 && + !content.includes('//') && + !content.includes('debugger;') + ) { + e.editor.session.setBreakpoint(row, undefined!); + } else { + e.editor.session.clearBreakpoint(row); + } + e.stop(); + handleEditorUpdateBreakpoints(e.editor.session.getBreakpoints()); + }; // Note: This is untestable/unused because JS-hint has been removed. const makeHandleAnnotationChange = (session: Ace.EditSession) => () => { diff --git a/src/commons/mobileWorkspace/MobileWorkspace.tsx b/src/commons/mobileWorkspace/MobileWorkspace.tsx index 46f91c0359..5f2d2e4c87 100644 --- a/src/commons/mobileWorkspace/MobileWorkspace.tsx +++ b/src/commons/mobileWorkspace/MobileWorkspace.tsx @@ -96,9 +96,8 @@ const MobileWorkspace: React.FC = props => { const editorRef = React.useRef(null); const replRef = React.useRef(null); const emptyRef = React.useRef(null); - const [keyboardInputRef, setKeyboardInputRef] = React.useState>( - emptyRef - ); + const [keyboardInputRef, setKeyboardInputRef] = + React.useState>(emptyRef); React.useEffect(() => { editorRef.current?.editor.on('focus', () => { diff --git a/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts b/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts index 96fef507d8..fec46804ac 100644 --- a/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts +++ b/src/commons/notificationBadge/__tests__/NotificationBadgeHelper.ts @@ -113,9 +113,8 @@ describe('filterNotificationsByType, ', () => { }); test('Grading works properly', () => { - const newNotifications = NotificationHelpers.filterNotificationsByType('Grading')( - notifications - ); + const newNotifications = + NotificationHelpers.filterNotificationsByType('Grading')(notifications); expect(newNotifications.length).toEqual(2); expect(newNotifications[0]).toEqual(notificationSubmissionMission); diff --git a/src/commons/sideContent/SideContentContestEntryCard.tsx b/src/commons/sideContent/SideContentContestEntryCard.tsx index 573b9caa8f..b96a467748 100644 --- a/src/commons/sideContent/SideContentContestEntryCard.tsx +++ b/src/commons/sideContent/SideContentContestEntryCard.tsx @@ -24,45 +24,46 @@ type StateProps = { * @param props functions for handling input and contest entry details tied to contest entry card. * @returns card which provides numeric input to vote for contest entry. */ -const SideContentContestEntryCard: React.FunctionComponent = props => { - const { - canSave, - isValid, - handleContestEntryClick, - handleVotingSubmissionChange, - contestEntry, - entryNumber, - maxRank - } = props; +const SideContentContestEntryCard: React.FunctionComponent = + props => { + const { + canSave, + isValid, + handleContestEntryClick, + handleVotingSubmissionChange, + contestEntry, + entryNumber, + maxRank + } = props; - return ( -
    - - handleContestEntryClick(contestEntry.submission_id, contestEntry.answer.code ?? '') - } - > -
    {entryNumber}
    -
    -          
    -              handleVotingSubmissionChange(contestEntry.submission_id, rank)
    -            }
    -            placeholder={`Enter rank for entry ${entryNumber}`}
    -            min={1}
    -            max={maxRank}
    -            allowNumericCharactersOnly
    -            fill
    -            minorStepSize={null} // limits input to integers
    -          />
    -        
    -
    -
    - ); -}; + return ( +
    + + handleContestEntryClick(contestEntry.submission_id, contestEntry.answer.code ?? '') + } + > +
    {entryNumber}
    +
    +            
    +                handleVotingSubmissionChange(contestEntry.submission_id, rank)
    +              }
    +              placeholder={`Enter rank for entry ${entryNumber}`}
    +              min={1}
    +              max={maxRank}
    +              allowNumericCharactersOnly
    +              fill
    +              minorStepSize={null} // limits input to integers
    +            />
    +          
    +
    +
    + ); + }; export default SideContentContestEntryCard; diff --git a/src/commons/sideContent/SideContentContestLeaderboard.tsx b/src/commons/sideContent/SideContentContestLeaderboard.tsx index 6dcf636fc9..074e0694fd 100644 --- a/src/commons/sideContent/SideContentContestLeaderboard.tsx +++ b/src/commons/sideContent/SideContentContestLeaderboard.tsx @@ -44,49 +44,52 @@ const contestLeaderboardTooltipContent = 'View the top-rated contest entries!'; * @param props {orderedContestEntries: an ordered list by desc score of leaderboard entries to display, * handleContestEntryClick: displays contest entry answer in assessment workspace editor} */ -const SideContentContestLeaderboard: React.FunctionComponent = props => { - const { orderedContestEntries, handleContestEntryClick } = props; - const [showLeaderboard, setShowLeaderboard] = useState(true); +const SideContentContestLeaderboard: React.FunctionComponent = + props => { + const { orderedContestEntries, handleContestEntryClick } = props; + const [showLeaderboard, setShowLeaderboard] = useState(true); - const contestEntryCards = useMemo( - () => ( -
    - {contestEntryHeader} - {orderedContestEntries.length > 0 ? ( - orderedContestEntries.map((contestEntry: ContestEntry, index: number) => ( - - )) - ) : ( -
    There are no eligible contest leaderboard entries found.
    - )} -
    - ), - [handleContestEntryClick, orderedContestEntries] - ); + const contestEntryCards = useMemo( + () => ( +
    + {contestEntryHeader} + {orderedContestEntries.length > 0 ? ( + orderedContestEntries.map((contestEntry: ContestEntry, index: number) => ( + + )) + ) : ( +
    + There are no eligible contest leaderboard entries found. +
    + )} +
    + ), + [handleContestEntryClick, orderedContestEntries] + ); - return ( -
    - - - {contestEntryCards} - -
    - ); -}; + return ( +
    + + + {contestEntryCards} + +
    + ); + }; export default SideContentContestLeaderboard; diff --git a/src/commons/sideContent/SideContentContestVotingContainer.tsx b/src/commons/sideContent/SideContentContestVotingContainer.tsx index 216ddb52cf..9ead82190e 100644 --- a/src/commons/sideContent/SideContentContestVotingContainer.tsx +++ b/src/commons/sideContent/SideContentContestVotingContainer.tsx @@ -20,68 +20,69 @@ type StateProps = { * Container to separate behaviour concerns from rendering concerns * Stores component-level voting ranking state */ -const SideContentContestVotingContainer: React.FunctionComponent = props => { - const { canSave, contestEntries, handleSave, handleContestEntryClick } = props; - const [isValid, setIsValid] = useState(true); - const [votingSubmission, setVotingSubmission] = useState([]); +const SideContentContestVotingContainer: React.FunctionComponent = + props => { + const { canSave, contestEntries, handleSave, handleContestEntryClick } = props; + const [isValid, setIsValid] = useState(true); + const [votingSubmission, setVotingSubmission] = useState([]); - useEffect(() => { - setVotingSubmission(contestEntries); - }, [contestEntries]); + useEffect(() => { + setVotingSubmission(contestEntries); + }, [contestEntries]); - /** - * Validates input value and clamps the value within the min-max range [1, number of entries]. - * @param votingSubmission voting scores by user for each contest entry. - * @returns boolean value for whether the scores are within the min-max range. - */ - const isSubmissionValid = (votingSubmission: ContestEntry[]) => { - return votingSubmission.reduce((isValid, vote) => { - return isValid && vote.rank! >= 1 && vote.rank! <= contestEntries.length; - }, true); - }; + /** + * Validates input value and clamps the value within the min-max range [1, number of entries]. + * @param votingSubmission voting scores by user for each contest entry. + * @returns boolean value for whether the scores are within the min-max range. + */ + const isSubmissionValid = (votingSubmission: ContestEntry[]) => { + return votingSubmission.reduce((isValid, vote) => { + return isValid && vote.rank! >= 1 && vote.rank! <= contestEntries.length; + }, true); + }; - const submissionHasNoNull = (votingSubmission: ContestEntry[]) => { - return votingSubmission.reduce((noNull, vote) => { - return noNull && vote.rank !== undefined && vote.rank !== null; - }, true); - }; + const submissionHasNoNull = (votingSubmission: ContestEntry[]) => { + return votingSubmission.reduce((noNull, vote) => { + return noNull && vote.rank !== undefined && vote.rank !== null; + }, true); + }; - const handleVotingSubmissionChange = (submissionId: number, rank: number): void => { - // update the votes - const updatedSubmission = votingSubmission.map(vote => - vote.submission_id === submissionId ? { ...vote, rank: rank } : vote - ); - setVotingSubmission(updatedSubmission); - const noDuplicates = - new Set(updatedSubmission.map(vote => vote.rank)).size === updatedSubmission.length; - // validate that scores are unique - const noNull = submissionHasNoNull(updatedSubmission); - if (noDuplicates && noNull && isSubmissionValid(updatedSubmission)) { - handleSave(updatedSubmission); - - setIsValid(true); - } else if (noDuplicates && noNull) { - showWarningMessage( - `Vote rankings invalid. Please input rankings between 1 - ${contestEntries.length}.` + const handleVotingSubmissionChange = (submissionId: number, rank: number): void => { + // update the votes + const updatedSubmission = votingSubmission.map(vote => + vote.submission_id === submissionId ? { ...vote, rank: rank } : vote ); - setIsValid(false); - } else if (!noDuplicates && noNull) { - showWarningMessage('Vote scores are not unique. Please input unique rankings.'); - setIsValid(false); - } else { - setIsValid(false); - } - }; + setVotingSubmission(updatedSubmission); + const noDuplicates = + new Set(updatedSubmission.map(vote => vote.rank)).size === updatedSubmission.length; + // validate that scores are unique + const noNull = submissionHasNoNull(updatedSubmission); + if (noDuplicates && noNull && isSubmissionValid(updatedSubmission)) { + handleSave(updatedSubmission); - return ( - - ); -}; + setIsValid(true); + } else if (noDuplicates && noNull) { + showWarningMessage( + `Vote rankings invalid. Please input rankings between 1 - ${contestEntries.length}.` + ); + setIsValid(false); + } else if (!noDuplicates && noNull) { + showWarningMessage('Vote scores are not unique. Please input unique rankings.'); + setIsValid(false); + } else { + setIsValid(false); + } + }; + + return ( + + ); + }; export default SideContentContestVotingContainer; diff --git a/src/commons/sideContent/SideContentLeaderboardCard.tsx b/src/commons/sideContent/SideContentLeaderboardCard.tsx index 3ba67c357a..548f6ffde0 100644 --- a/src/commons/sideContent/SideContentLeaderboardCard.tsx +++ b/src/commons/sideContent/SideContentLeaderboardCard.tsx @@ -15,24 +15,25 @@ type StateProps = { rank: number; }; -const SideContentLeaderboardCard: React.FunctionComponent = props => { - const { handleContestEntryClick, contestEntry, rank } = props; +const SideContentLeaderboardCard: React.FunctionComponent = + props => { + const { handleContestEntryClick, contestEntry, rank } = props; - return ( -
    - - handleContestEntryClick(contestEntry.submission_id, contestEntry.answer.code ?? '') - } - > -
    {contestEntry.student_name}
    -
    {rank}
    -
    {contestEntry.score}
    -
    -
    - ); -}; + return ( +
    + + handleContestEntryClick(contestEntry.submission_id, contestEntry.answer.code ?? '') + } + > +
    {contestEntry.student_name}
    +
    {rank}
    +
    {contestEntry.score}
    +
    +
    + ); + }; export default SideContentLeaderboardCard; diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts index 8be1da8644..d6bd8f9ff8 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -31,10 +31,8 @@ const googleAppId = process.env.REACT_APP_GOOGLE_APP_ID; const githubClientId = process.env.REACT_APP_GITHUB_CLIENT_ID || ''; const githubOAuthProxyUrl = process.env.REACT_APP_GITHUB_OAUTH_PROXY_URL || ''; -const authProviders: Map< - string, - { name: string; endpoint: string; isDefault: boolean } -> = new Map(); +const authProviders: Map = + new Map(); for (let i = 1; ; ++i) { const id = process.env[`REACT_APP_OAUTH2_PROVIDER${i}`]; diff --git a/src/commons/utils/Hooks.ts b/src/commons/utils/Hooks.ts index 372e71068b..2c9192895e 100644 --- a/src/commons/utils/Hooks.ts +++ b/src/commons/utils/Hooks.ts @@ -26,13 +26,13 @@ import React from 'react'; // The following hook is from // https://github.com/jaredLunde/react-hook/blob/master/packages/merged-ref/src/index.tsx -export const useMergedRef = (...refs: React.Ref[]): React.RefCallback => ( - element: T -) => - refs.forEach(ref => { - if (typeof ref === 'function') ref(element); - else if (ref && typeof ref === 'object') (ref as React.MutableRefObject).current = element; - }); +export const useMergedRef = + (...refs: React.Ref[]): React.RefCallback => + (element: T) => + refs.forEach(ref => { + if (typeof ref === 'function') ref(element); + else if (ref && typeof ref === 'object') (ref as React.MutableRefObject).current = element; + }); // End diff --git a/src/features/eventLogging/index.ts b/src/features/eventLogging/index.ts index 97a44feac6..b955b9e0d7 100644 --- a/src/features/eventLogging/index.ts +++ b/src/features/eventLogging/index.ts @@ -59,33 +59,31 @@ export type LoggedRecord = LogRecord & { id: number }; const VERSION = 1; const DB_NAME = 'evtlogs'; const STORE_NAME = 'logs'; -const getDB = memoize( - (): Promise => { - return new Promise((resolve, reject) => { - // Make a request - const request = indexedDB.open(DB_NAME, VERSION); - // hook the onsuccess - request.onsuccess = evt => { - resolve(request.result); - }; - - request.onerror = evt => { - console.error('Failed to get db', evt); - reject(request.error); - }; - - // Set it up if necessary (on upgrade) - request.onupgradeneeded = evt => { - // Create the database here - const db: IDBDatabase = (evt?.target as any).result; // Bug with the types... - db.createObjectStore(STORE_NAME, { - keyPath: 'id', // Entry id, only used to figure out the last transfered value - autoIncrement: true - }); - }; - }); - } -); +const getDB = memoize((): Promise => { + return new Promise((resolve, reject) => { + // Make a request + const request = indexedDB.open(DB_NAME, VERSION); + // hook the onsuccess + request.onsuccess = evt => { + resolve(request.result); + }; + + request.onerror = evt => { + console.error('Failed to get db', evt); + reject(request.error); + }; + + // Set it up if necessary (on upgrade) + request.onupgradeneeded = evt => { + // Create the database here + const db: IDBDatabase = (evt?.target as any).result; // Bug with the types... + db.createObjectStore(STORE_NAME, { + keyPath: 'id', // Entry id, only used to figure out the last transfered value + autoIncrement: true + }); + }; + }); +}); function saveRecord(record: LogRecord) { return new Promise((resolve, reject) => { diff --git a/src/features/game/action/GameActionManager.ts b/src/features/game/action/GameActionManager.ts index db7e5b61c1..a630130101 100644 --- a/src/features/game/action/GameActionManager.ts +++ b/src/features/game/action/GameActionManager.ts @@ -44,13 +44,8 @@ export default class GameActionManager { * @param actionId id of the action */ public async processGameAction(actionId: ItemId) { - const { - actionType, - actionParams, - actionConditions, - isRepeatable, - interactionId - } = GameGlobalAPI.getInstance().getActionById(actionId); + const { actionType, actionParams, actionConditions, isRepeatable, interactionId } = + GameGlobalAPI.getInstance().getActionById(actionId); if (await this.checkCanPlayAction(isRepeatable, interactionId, actionConditions)) { await GameActionExecuter.executeGameAction(actionType, actionParams); diff --git a/src/features/game/dialogue/GameDialogueManager.ts b/src/features/game/dialogue/GameDialogueManager.ts index 3911462790..6d4218cd67 100644 --- a/src/features/game/dialogue/GameDialogueManager.ts +++ b/src/features/game/dialogue/GameDialogueManager.ts @@ -51,12 +51,8 @@ export default class DialogueManager { private async showNextLine(resolve: () => void) { GameGlobalAPI.getInstance().playSound(SoundAssets.dialogueAdvance.key); - const { - line, - speakerDetail, - actionIds, - prompt - } = this.getDialogueGenerator().generateNextLine(); + const { line, speakerDetail, actionIds, prompt } = + this.getDialogueGenerator().generateNextLine(); const lineWithName = line.replace('{name}', this.getUsername()); this.getDialogueRenderer().changeText(lineWithName); this.getSpeakerRenderer().changeSpeakerTo(speakerDetail); diff --git a/src/features/game/parser/ActionParser.ts b/src/features/game/parser/ActionParser.ts index 2c8dd3563b..6b6cccebf1 100644 --- a/src/features/game/parser/ActionParser.ts +++ b/src/features/game/parser/ActionParser.ts @@ -37,10 +37,9 @@ export default class ActionParser { const gameAction = this.parseActionContent(actionString); if (conditionalsString) { - gameAction.actionConditions = StringUtils.splitByChar( - conditionalsString, - 'AND' - ).map(condition => ConditionParser.parse(condition)); + gameAction.actionConditions = StringUtils.splitByChar(conditionalsString, 'AND').map( + condition => ConditionParser.parse(condition) + ); } Parser.checkpoint.map.setItemInMap(GameItemType.actions, gameAction.interactionId, gameAction); diff --git a/src/features/game/parser/ParserValidator.ts b/src/features/game/parser/ParserValidator.ts index 6bd5c31e91..bfc7ac3d3b 100644 --- a/src/features/game/parser/ParserValidator.ts +++ b/src/features/game/parser/ParserValidator.ts @@ -192,8 +192,9 @@ export default class ParserValidator { case GameEntityType.bgms: const numberOfBgm = Parser.checkpoint.map .getSoundAssets() - .filter(sound => sound.soundType === GameSoundType.BGM && sound.key === itemId) - .length; + .filter( + sound => sound.soundType === GameSoundType.BGM && sound.key === itemId + ).length; if (numberOfBgm === 0) { throw new Error(`Cannot find bgm key "${itemId}"`); } else if (numberOfBgm > 1) { @@ -204,8 +205,9 @@ export default class ParserValidator { case GameEntityType.sfxs: const numberOfSfx = Parser.checkpoint.map .getSoundAssets() - .filter(sound => sound.soundType === GameSoundType.SFX && sound.key === itemId) - .length; + .filter( + sound => sound.soundType === GameSoundType.SFX && sound.key === itemId + ).length; if (numberOfSfx === 0) { throw new Error(`Cannot find sfx key "${itemId}"`); } else if (numberOfSfx > 1) { diff --git a/src/features/storySimulator/StorySimulatorRequest.ts b/src/features/storySimulator/StorySimulatorRequest.ts index 8c088a8c44..22f2c0bd25 100644 --- a/src/features/storySimulator/StorySimulatorRequest.ts +++ b/src/features/storySimulator/StorySimulatorRequest.ts @@ -2,30 +2,32 @@ import Constants from 'src/commons/utils/Constants'; import SourceAcademyGame from '../game/SourceAcademyGame'; -const sendRequest = (route: string) => async ( - requestPath: string, - method: string, - headerConfig: object = {}, - requestDetails: object = {} -) => { - try { - const accessToken = SourceAcademyGame.getInstance().getAccountInfo().accessToken || ''; +const sendRequest = + (route: string) => + async ( + requestPath: string, + method: string, + headerConfig: object = {}, + requestDetails: object = {} + ) => { + try { + const accessToken = SourceAcademyGame.getInstance().getAccountInfo().accessToken || ''; - const headers = createHeaders(accessToken); - Object.entries(headerConfig).forEach(([key, value]: string[]) => { - headers.append(key, value); - }); + const headers = createHeaders(accessToken); + Object.entries(headerConfig).forEach(([key, value]: string[]) => { + headers.append(key, value); + }); - const config = { - method, - headers, - ...requestDetails - }; + const config = { + method, + headers, + ...requestDetails + }; - return fetch(Constants.backendUrl + `/v2/${route}/` + requestPath, config); - } finally { - } -}; + return fetch(Constants.backendUrl + `/v2/${route}/` + requestPath, config); + } finally { + } + }; export const sendAssetRequest = sendRequest('admin/assets'); export const sendStoryRequest = sendRequest('stories'); diff --git a/src/pages/academy/Academy.tsx b/src/pages/academy/Academy.tsx index f2322329f6..b47bfc4a0e 100644 --- a/src/pages/academy/Academy.tsx +++ b/src/pages/academy/Academy.tsx @@ -89,9 +89,9 @@ class Academy extends React.Component { ); } - private assessmentRenderFactory = (cat: AssessmentCategory) => ( - routerProps: RouteComponentProps - ) => ; + private assessmentRenderFactory = + (cat: AssessmentCategory) => (routerProps: RouteComponentProps) => + ; /** * 1. If user is in /academy.*, redirect to game diff --git a/src/pages/academy/grading/subcomponents/GradingEditor.tsx b/src/pages/academy/grading/subcomponents/GradingEditor.tsx index 1b233b1627..997c627403 100644 --- a/src/pages/academy/grading/subcomponents/GradingEditor.tsx +++ b/src/pages/academy/grading/subcomponents/GradingEditor.tsx @@ -305,33 +305,35 @@ class GradingEditor extends React.Component { * returning the relevant saving function (for the 'Save Draft' * and 'Submit and Continue' buttons) */ - private validateGradesBeforeSave = (handleSaving: GradingSaveFunction): (() => void) => () => { - const gradeAdjustmentInput = - stringParamToInt(this.state.gradeAdjustmentInput || undefined) || undefined; - const grade = this.props.initialGrade + (gradeAdjustmentInput || 0); - const xpAdjustmentInput = - stringParamToInt(this.state.xpAdjustmentInput || undefined) || undefined; - const xp = this.props.initialXp + (xpAdjustmentInput || 0); - if (grade < 0 || grade > this.props.maxGrade) { - showWarningMessage( - `Grade ${grade.toString()} is out of bounds. Maximum grade is ${this.props.maxGrade.toString()}.` - ); - return; - } else if (xp < 0 || xp > this.props.maxXp) { - showWarningMessage( - `XP ${xp.toString()} is out of bounds. Maximum xp is ${this.props.maxXp.toString()}.` - ); - return; - } else { - handleSaving( - this.props.submissionId, - this.props.questionId, - gradeAdjustmentInput, - xpAdjustmentInput, - this.state.editorValue! - ); - } - }; + private validateGradesBeforeSave = + (handleSaving: GradingSaveFunction): (() => void) => + () => { + const gradeAdjustmentInput = + stringParamToInt(this.state.gradeAdjustmentInput || undefined) || undefined; + const grade = this.props.initialGrade + (gradeAdjustmentInput || 0); + const xpAdjustmentInput = + stringParamToInt(this.state.xpAdjustmentInput || undefined) || undefined; + const xp = this.props.initialXp + (xpAdjustmentInput || 0); + if (grade < 0 || grade > this.props.maxGrade) { + showWarningMessage( + `Grade ${grade.toString()} is out of bounds. Maximum grade is ${this.props.maxGrade.toString()}.` + ); + return; + } else if (xp < 0 || xp > this.props.maxXp) { + showWarningMessage( + `XP ${xp.toString()} is out of bounds. Maximum xp is ${this.props.maxXp.toString()}.` + ); + return; + } else { + handleSaving( + this.props.submissionId, + this.props.questionId, + gradeAdjustmentInput, + xpAdjustmentInput, + this.state.editorValue! + ); + } + }; /** * Sets the state currentlySaving to true to disable diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlDropzone.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlDropzone.tsx index fae47f9baa..a165e3ce8b 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlDropzone.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlDropzone.tsx @@ -42,18 +42,12 @@ const MaterialDropzone: React.FunctionComponent = props => { } }, []); - const { - getRootProps, - getInputProps, - isFocused, - isDragActive, - isDragAccept, - isDragReject - } = useDropzone({ - multiple: false, - onDropAccepted: handleDropAccepted, - onDropRejected: handleDropRejected - }); + const { getRootProps, getInputProps, isFocused, isDragActive, isDragAccept, isDragReject } = + useDropzone({ + multiple: false, + onDropAccepted: handleDropAccepted, + onDropRejected: handleDropRejected + }); const classList = React.useMemo(() => { return classNames( From 4a96cbaa5276392e37598e11265d49dcff0229ad Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Thu, 20 May 2021 17:15:25 +0800 Subject: [PATCH 141/143] Added type annotations to AchievementSaga --- src/commons/sagas/AchievementSaga.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commons/sagas/AchievementSaga.ts b/src/commons/sagas/AchievementSaga.ts index 18e532556a..a9104cc092 100644 --- a/src/commons/sagas/AchievementSaga.ts +++ b/src/commons/sagas/AchievementSaga.ts @@ -149,7 +149,7 @@ export default function* AchievementSaga(): SagaIterator { } }); - yield takeEvery(GET_USERS, function* (action: ReturnType) { + yield takeEvery(GET_USERS, function* (action: ReturnType): any { const tokens = yield select((state: OverallState) => ({ accessToke: state.session.accessToken, refreshToken: state.session.refreshToken @@ -197,7 +197,7 @@ export default function* AchievementSaga(): SagaIterator { yield takeEvery( UPDATE_OWN_GOAL_PROGRESS, - function* (action: ReturnType) { + function* (action: ReturnType): any { const tokens = yield select((state: OverallState) => ({ accessToken: state.session.accessToken, refreshToken: state.session.refreshToken @@ -231,7 +231,7 @@ export default function* AchievementSaga(): SagaIterator { } ); - yield takeEvery(HANDLE_EVENT, function* (action: ReturnType) { + yield takeEvery(HANDLE_EVENT, function* (action: ReturnType): any { const tokens = yield select((state: OverallState) => ({ accessToken: state.session.accessToken, refreshToken: state.session.refreshToken From 2d89b38e14a89ae4aa5f80328aa2ec2a9302e47f Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 31 May 2021 14:54:27 +0800 Subject: [PATCH 142/143] Added condition to check for login in events --- src/commons/achievement/utils/EventHandler.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/commons/achievement/utils/EventHandler.ts b/src/commons/achievement/utils/EventHandler.ts index 176a3d6cd0..1afb233e2c 100644 --- a/src/commons/achievement/utils/EventHandler.ts +++ b/src/commons/achievement/utils/EventHandler.ts @@ -8,6 +8,7 @@ import { GoalType } from '../../../features/achievement/AchievementTypes'; import { store } from '../../../pages/createStore'; +import Constants from '../../utils/Constants'; import { showSuccessMessage } from '../../utils/NotificationsHelper'; import AchievementInferencer from './AchievementInferencer'; import { isExpired, isReleased, isWithinTimeRange } from './DateHelper'; @@ -74,13 +75,15 @@ function resetLoggedEvents() { } export function processEvent(eventNames: EventType[]) { - loggedEvents.push(eventNames); + if (store.getState().session.role && !Constants.playgroundOnly && Constants.enableAchievements) { + loggedEvents.push(eventNames); - if (!timeoutSet) { - timeoutSet = true; - setTimeout(() => { - store.dispatch(handleEvent(loggedEvents)); - resetLoggedEvents(); - }, updateInterval); + if (!timeoutSet) { + timeoutSet = true; + setTimeout(() => { + store.dispatch(handleEvent(loggedEvents)); + resetLoggedEvents(); + }, updateInterval); + } } } From 836492657d262333c8016e51dd90bc7afceb5561 Mon Sep 17 00:00:00 2001 From: Jonas Chow Date: Mon, 31 May 2021 16:15:45 +0800 Subject: [PATCH 143/143] Used moment.js in achievement DateHelper. --- src/commons/achievement/utils/DateHelper.ts | 50 ++++----------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/src/commons/achievement/utils/DateHelper.ts b/src/commons/achievement/utils/DateHelper.ts index 2f00d529e0..1d695f1a02 100644 --- a/src/commons/achievement/utils/DateHelper.ts +++ b/src/commons/achievement/utils/DateHelper.ts @@ -1,3 +1,5 @@ +import moment from 'moment'; + const now = new Date(); export const isExpired = (deadline?: Date) => deadline !== undefined && deadline <= now; @@ -36,61 +38,27 @@ export const timeFromExpired = (deadline?: Date) => export const prettifyDate = (deadline?: Date) => { if (deadline === undefined) return ''; - const months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ]; - const day = deadline.getDate(); - const month = months[deadline.getMonth()]; - const year = deadline.getFullYear(); - const hour = deadline.getHours(); - const minute = deadline.getMinutes(); - const time = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute; - - return `${day} ${month} ${year} ${time}`; + return moment(deadline).format('D MMMM YYYY HH:mm'); }; export const prettifyTime = (time?: Date) => { if (time === undefined) return ''; - const hour = time.getHours(); - const minute = time.getMinutes(); - return (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute; + return moment(time).format('HH:mm'); }; // Converts Date to deadline countdown export const prettifyDeadline = (deadline?: Date) => { - /* ---------- Date constants ---------- */ - const daysPerWeek = 7; - const hoursPerDay = 24; - const millisecondsPerHour = 3600000; - - /* -------- Helper for Deadline -------- */ - const isExpired = (deadline: Date): boolean => deadline.getTime() <= now.getTime(); - const getHoursAway = (deadline: Date): number => - (deadline.getTime() - now.getTime()) / millisecondsPerHour; - const getDaysAway = (deadline: Date): number => getHoursAway(deadline) / hoursPerDay; - const getWeeksAway = (deadline: Date): number => getDaysAway(deadline) / daysPerWeek; - - /* -------- Prettifies Deadline -------- */ if (deadline === undefined) { return 'Unlimited'; } else if (isExpired(deadline)) { return 'Expired'; } - const weeksAway = Math.ceil(getWeeksAway(deadline)); - const daysAway = Math.ceil(getDaysAway(deadline)); - const hoursAway = Math.ceil(getHoursAway(deadline)); + const now = moment(); + + const weeksAway = Math.ceil(moment(deadline).diff(now, 'weeks', true)); + const daysAway = Math.ceil(moment(deadline).diff(now, 'days', true)); + const hoursAway = Math.ceil(moment(deadline).diff(now, 'hours', true)); let prettifiedDeadline = ''; if (weeksAway > 1) {