Skip to content

Commit b63ff09

Browse files
jonas-chowchownces
authored andcommitted
Achievement UI update
1 parent c8e5c30 commit b63ff09

File tree

12 files changed

+127
-57
lines changed

12 files changed

+127
-57
lines changed

src/commons/achievement/AchievementManualEditor.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Button, MenuItem, NumericInput } from '@blueprintjs/core';
2-
import { ItemRenderer, Select } from '@blueprintjs/select';
1+
import { Button, Checkbox, MenuItem, NumericInput } from '@blueprintjs/core';
2+
import { ItemPredicate, ItemRenderer, Select } from '@blueprintjs/select';
33
import { useContext, useEffect, useState } from 'react';
44
import { AchievementContext } from 'src/features/achievement/AchievementConstants';
55
import {
@@ -8,7 +8,11 @@ import {
88
GoalProgress
99
} from 'src/features/achievement/AchievementTypes';
1010

11+
import { showSuccessMessage, showWarningMessage } from '../utils/NotificationsHelper';
12+
1113
type AchievementManualEditorProps = {
14+
hiddenState: [boolean, any];
15+
userState: [AchievementUser | undefined, any];
1216
studio: string;
1317
users: AchievementUser[];
1418
getUsers: () => void;
@@ -19,9 +23,22 @@ const GoalSelect = Select.ofType<AchievementGoal>();
1923
const goalRenderer: ItemRenderer<AchievementGoal> = (goal, { handleClick }) => (
2024
<MenuItem key={goal.uuid} onClick={handleClick} text={goal.text} />
2125
);
26+
const goalPredicate: ItemPredicate<AchievementGoal> = (query, item) =>
27+
item.text.toLowerCase().includes(query.toLowerCase());
28+
29+
const UserSelect = Select.ofType<AchievementUser>();
30+
const userRenderer: ItemRenderer<AchievementUser> = (user, { handleClick }) => (
31+
<MenuItem key={user.courseRegId} onClick={handleClick} text={user.name} />
32+
);
33+
const userPredicate: ItemPredicate<AchievementUser> = (query, item) =>
34+
item.name.toLowerCase().includes(query.toLowerCase());
35+
36+
export function updateGoalProcessed() {
37+
showSuccessMessage('Goal updated');
38+
}
2239

2340
function AchievementManualEditor(props: AchievementManualEditorProps) {
24-
const { studio, getUsers, updateGoalProgress } = props;
41+
const { userState, hiddenState, studio, getUsers, updateGoalProgress } = props;
2542
const users =
2643
studio === 'Staff'
2744
? // The name can be null for users who have yet to log in. We push these to the back of the array.
@@ -39,35 +56,36 @@ function AchievementManualEditor(props: AchievementManualEditorProps) {
3956
.getAllGoals()
4057
.filter(goals => goals.meta.type === 'Manual');
4158

42-
const [goal, changeGoal] = useState(manualAchievements[0]);
43-
const [selectedUser, changeSelectedUser] = useState(users[0]);
44-
const [count, changeCount] = useState(0);
45-
46-
const UserSelect = Select.ofType<AchievementUser>();
47-
const userRenderer: ItemRenderer<AchievementUser> = (user, { handleClick }) => (
48-
<MenuItem key={user.courseRegId} onClick={handleClick} text={user.name} />
49-
);
59+
const [goal, changeGoal] = useState<AchievementGoal | undefined>(undefined);
60+
const [selectedUser, changeSelectedUser] = userState;
61+
const [count, changeCount] = useState<number>(0);
62+
const [viewHidden, changeViewHidden] = hiddenState;
5063

5164
const updateGoal = () => {
52-
if (goal) {
65+
if (goal && selectedUser) {
5366
const progress: GoalProgress = {
5467
uuid: goal.uuid,
55-
count: count,
68+
count: count < 0 ? 0 : Math.floor(count),
5669
targetCount: goal.targetCount,
5770
completed: count >= goal.targetCount
5871
};
5972
updateGoalProgress(selectedUser.courseRegId, progress);
73+
} else {
74+
!goal && showWarningMessage('Goal not selected');
75+
!selectedUser && showWarningMessage('User not selected');
6076
}
6177
};
6278

6379
return (
6480
<div className="achievement-manual-editor">
6581
<h3>User: </h3>
6682
<UserSelect
67-
filterable={false}
83+
filterable={true}
6884
items={users}
6985
itemRenderer={userRenderer}
86+
itemPredicate={userPredicate}
7087
onItemSelect={changeSelectedUser}
88+
noResults={<MenuItem disabled={true} text="No matching user" />}
7189
>
7290
<Button
7391
outlined={true}
@@ -78,10 +96,12 @@ function AchievementManualEditor(props: AchievementManualEditorProps) {
7896

7997
<h3>Goal: </h3>
8098
<GoalSelect
81-
filterable={false}
99+
filterable={true}
82100
items={manualAchievements}
83101
itemRenderer={goalRenderer}
102+
itemPredicate={goalPredicate}
84103
onItemSelect={changeGoal}
104+
noResults={<MenuItem disabled={true} text="No matching goal" />}
85105
>
86106
<Button outlined={true} text={goal ? goal.text : 'No Goal Selected'} color="White" />
87107
</GoalSelect>
@@ -91,12 +111,20 @@ function AchievementManualEditor(props: AchievementManualEditorProps) {
91111
value={count}
92112
min={0}
93113
allowNumericCharactersOnly={true}
114+
minorStepSize={null}
94115
placeholder="Count"
95116
onValueChange={changeCount}
96117
/>
97118

98119
<h3> </h3>
99120
<Button outlined={true} text="Update Goal" onClick={updateGoal} intent="primary" />
121+
122+
<h3> </h3>
123+
<Checkbox
124+
checked={viewHidden}
125+
label="View Hidden Achievements"
126+
onChange={() => changeViewHidden(!viewHidden)}
127+
/>
100128
</div>
101129
);
102130
}

src/commons/achievement/AchievementView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ function AchievementView(props: AchievementViewProps) {
4444
<div
4545
className="cover"
4646
style={{
47-
background: `url(${coverImage}) center/cover`
47+
background: `rgba(0, 0, 0, 0.5) url(${coverImage}) center/cover`,
48+
backgroundBlendMode: `darken`
4849
}}
4950
>
5051
<h1>{title.toUpperCase()}</h1>

src/commons/achievement/control/achievementEditor/AchievementSettings.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Dialog, EditableText } from '@blueprintjs/core';
1+
import { Button, Checkbox, Dialog, EditableText } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33
import { Tooltip2 } from '@blueprintjs/popover2';
44
import { useState } from 'react';
@@ -58,9 +58,13 @@ function AchievementSettings(props: AchievementSettingsProps) {
5858
/>
5959
<h3>Goals</h3>
6060
<EditableGoalUuids changeGoalUuids={changeGoalUuids} goalUuids={goalUuids} />
61-
<Tooltip2 content="The rewarded XP will be equal to the sum of 'count' of goals">
62-
<Button text={'Variable XP?'} active={isVariableXp} onClick={changeIsVariableXp} />
63-
</Tooltip2>
61+
62+
<h3>Variable XP</h3>
63+
<Checkbox
64+
label={"The rewarded XP will be equal to the sum of 'count' of goals"}
65+
checked={isVariableXp}
66+
onChange={changeIsVariableXp}
67+
/>
6468
</div>
6569
</Dialog>
6670
</>

src/commons/achievement/control/achievementEditor/achievementSettings/EditableGoalUuids.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { MenuItem } from '@blueprintjs/core';
2-
import { ItemRenderer, MultiSelect } from '@blueprintjs/select';
2+
import { ItemPredicate, ItemRenderer, MultiSelect } from '@blueprintjs/select';
33
import { without } from 'lodash';
44
import { useContext } from 'react';
55
import { AchievementContext } from 'src/features/achievement/AchievementConstants';
6+
import { AchievementGoal } from 'src/features/achievement/AchievementTypes';
67

78
type EditableGoalUuidsProps = {
89
changeGoalUuids: (goalUuids: string[]) => void;
@@ -19,12 +20,13 @@ function EditableGoalUuids(props: EditableGoalUuidsProps) {
1920
);
2021

2122
const getUuid = (text: string) => inferencer.getUuidByText(text);
22-
const getText = (uuid: string) => inferencer.getTextByUuid(uuid);
2323

24-
const GoalSelect = MultiSelect.ofType<string>();
25-
const goalRenderer: ItemRenderer<string> = (uuid, { handleClick }) => (
26-
<MenuItem key={uuid} onClick={handleClick} text={getText(uuid)} />
24+
const GoalSelect = MultiSelect.ofType<AchievementGoal>();
25+
const goalRenderer: ItemRenderer<AchievementGoal> = (goal, { handleClick }) => (
26+
<MenuItem key={goal.uuid} onClick={handleClick} text={goal.text} />
2727
);
28+
const goalPredicate: ItemPredicate<AchievementGoal> = (query, item) =>
29+
item.text.toLowerCase().includes(query.toLowerCase());
2830

2931
const selectedGoals = new Set(selectedUuids);
3032
const availableGoals = new Set(without(allGoalUuids, ...goalUuids));
@@ -36,22 +38,24 @@ function EditableGoalUuids(props: EditableGoalUuidsProps) {
3638
};
3739

3840
const removeGoal = (removeUuid?: string) => {
39-
if (removeUuid === undefined) return;
41+
if (removeGoal === undefined) return;
4042

41-
selectedGoals.delete(removeUuid);
42-
availableGoals.add(removeUuid);
43+
selectedGoals.delete(removeUuid!);
44+
availableGoals.add(removeUuid!);
4345
changeGoalUuids([...selectedGoals]);
4446
};
4547

4648
return (
4749
<GoalSelect
4850
itemRenderer={goalRenderer}
49-
items={[...availableGoals]}
51+
items={[...availableGoals].map(uuid => inferencer.getGoal(uuid))}
5052
noResults={<MenuItem disabled={true} text="No available goal" />}
51-
onItemSelect={selectGoal}
52-
selectedItems={[...selectedGoals]}
53+
onItemSelect={goal => selectGoal(goal.uuid)}
54+
selectedItems={[...selectedGoals].map(uuid => inferencer.getGoal(uuid))}
5355
tagInputProps={{ onRemove: text => removeGoal(getUuid(text!.toString())) }}
54-
tagRenderer={getText}
56+
tagRenderer={goal => goal.text}
57+
itemPredicate={goalPredicate}
58+
resetOnSelect={true}
5559
/>
5660
);
5761
}

src/commons/achievement/control/achievementEditor/achievementSettings/EditablePrerequisiteUuids.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { MenuItem } from '@blueprintjs/core';
2-
import { ItemRenderer, MultiSelect } from '@blueprintjs/select';
2+
import { ItemPredicate, ItemRenderer, MultiSelect } from '@blueprintjs/select';
3+
import { without } from 'lodash';
34
import { useContext } from 'react';
45
import { AchievementContext } from 'src/features/achievement/AchievementConstants';
6+
import { AchievementItem } from 'src/features/achievement/AchievementTypes';
57

68
type EditablePrerequisiteUuidsProps = {
79
changePrerequisiteUuids: (prerequisiteUuids: string[]) => void;
@@ -19,15 +21,16 @@ function EditablePrerequisiteUuids(props: EditablePrerequisiteUuidsProps) {
1921
);
2022

2123
const getUuid = (title: string) => inferencer.getUuidByTitle(title);
22-
const getTitle = (uuid: string) => inferencer.getTitleByUuid(uuid);
2324

24-
const PrerequisiteSelect = MultiSelect.ofType<string>();
25-
const prerequisiteRenderer: ItemRenderer<string> = (uuid, { handleClick }) => (
26-
<MenuItem key={uuid} onClick={handleClick} text={getTitle(uuid)} />
25+
const PrerequisiteSelect = MultiSelect.ofType<AchievementItem>();
26+
const prerequisiteRenderer: ItemRenderer<AchievementItem> = (achievement, { handleClick }) => (
27+
<MenuItem key={achievement.uuid} onClick={handleClick} text={achievement.title} />
2728
);
29+
const prerequisitePredicate: ItemPredicate<AchievementItem> = (query, item) =>
30+
item.title.toLowerCase().includes(query.toLowerCase());
2831

2932
const selectedPrereqs = new Set(selectedUuids);
30-
const availablePrereqs = new Set(availableUuids);
33+
const availablePrereqs = new Set(without(availableUuids, ...selectedUuids));
3134

3235
const selectPrereq = (selectUuid: string) => {
3336
selectedPrereqs.add(selectUuid);
@@ -46,12 +49,14 @@ function EditablePrerequisiteUuids(props: EditablePrerequisiteUuidsProps) {
4649
return (
4750
<PrerequisiteSelect
4851
itemRenderer={prerequisiteRenderer}
49-
items={[...availablePrereqs]}
52+
items={[...availablePrereqs].map(uuid => inferencer.getAchievement(uuid))}
5053
noResults={<MenuItem disabled={true} text="No available achievement" />}
51-
onItemSelect={selectPrereq}
52-
selectedItems={[...selectedPrereqs]}
54+
onItemSelect={achievement => selectPrereq(achievement.uuid)}
55+
selectedItems={[...selectedPrereqs].map(uuid => inferencer.getAchievement(uuid))}
5356
tagInputProps={{ onRemove: title => removePrereq(getUuid(title!.toString())) }}
54-
tagRenderer={getTitle}
57+
tagRenderer={achievement => achievement.title}
58+
itemPredicate={prerequisitePredicate}
59+
resetOnSelect={true}
5560
/>
5661
);
5762
}

src/commons/achievement/utils/AchievementInferencer.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,15 @@ class AchievementInferencer {
495495
.map(sortedTask => sortedTask.uuid);
496496
}
497497

498+
/**
499+
* Returns an array of achievementId sorted by position
500+
*/
501+
public listAllSortedAchievementUuids() {
502+
return this.getAllAchievements()
503+
.sort((taskA, taskB) => taskA.position - taskB.position)
504+
.map(sortedTask => sortedTask.uuid);
505+
}
506+
498507
/**
499508
* Returns whether an achievement is completed or not.
500509
*

src/commons/achievement/utils/InsertFakeAchievements.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
cardBackgroundUrl,
3-
coverImageUrl
4-
} from '../../../features/achievement/AchievementConstants';
1+
import { cardBackgroundUrl } from '../../../features/achievement/AchievementConstants';
52
import { AchievementAbility, GoalType } from '../../../features/achievement/AchievementTypes';
63
import { AssessmentOverview } from '../../assessment/AssessmentTypes';
74
import AchievementInferencer from './AchievementInferencer';
@@ -69,7 +66,7 @@ function insertFakeAchievements(
6966
goalUuids: [idString + '0', idString + '1'], // need to create a mock completed goal to reference to be considered complete
7067
cardBackground: `${cardBackgroundUrl}/default.png`,
7168
view: {
72-
coverImage: `${coverImageUrl}/default.png`,
69+
coverImage: assessmentOverview.coverImage,
7370
description: assessmentOverview.shortSummary,
7471
completionText: `XP: ${assessmentOverview.xp} / ${assessmentOverview.maxXp}`
7572
}

src/commons/sagas/AchievementSaga.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
UPDATE_GOAL_PROGRESS,
1818
UPDATE_OWN_GOAL_PROGRESS
1919
} from '../../features/achievement/AchievementTypes';
20+
import { updateGoalProcessed } from '../achievement/AchievementManualEditor';
2021
import AchievementInferencer from '../achievement/utils/AchievementInferencer';
2122
import { goalIncludesEvents, incrementCount } from '../achievement/utils/EventHandler';
2223
import { OverallState } from '../application/ApplicationTypes';
@@ -88,9 +89,9 @@ export default function* AchievementSaga(): SagaIterator {
8889
yield takeEvery(GET_GOALS, function* (action: ReturnType<typeof actions.getGoals>): any {
8990
const tokens: Tokens = yield selectTokens();
9091

91-
const studentId = action.payload;
92+
const studentCourseRegId = action.payload;
9293

93-
const goals = yield call(getGoals, tokens, studentId);
94+
const goals = yield call(getGoals, tokens, studentCourseRegId);
9495

9596
if (goals) {
9697
yield put(actions.saveGoals(goals));
@@ -171,6 +172,10 @@ export default function* AchievementSaga(): SagaIterator {
171172
if (!resp) {
172173
return;
173174
}
175+
if (resp.ok) {
176+
yield put(actions.getGoals(studentCourseRegId));
177+
updateGoalProcessed();
178+
}
174179
}
175180
);
176181

src/commons/sagas/RequestsSaga.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export const getGoals = async (
247247
tokens: Tokens,
248248
studentId: number
249249
): Promise<AchievementGoal[] | null> => {
250-
const resp = await request(`${courseId()}/achievements/goals/${studentId}`, 'GET', {
250+
const resp = await request(`${courseId()}/admin/goals/${studentId}`, 'GET', {
251251
...tokens,
252252
shouldRefresh: true
253253
});

src/features/achievement/AchievementActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const bulkUpdateGoals = (goals: GoalDefinition[]) => action(BULK_UPDATE_G
3131

3232
export const getAchievements = () => action(GET_ACHIEVEMENTS);
3333

34-
export const getGoals = (studentId: number) => action(GET_GOALS, studentId);
34+
export const getGoals = (studentCourseRegId: number) => action(GET_GOALS, studentCourseRegId);
3535

3636
export const getOwnGoals = () => action(GET_OWN_GOALS);
3737

0 commit comments

Comments
 (0)