Skip to content

Commit 1a4198a

Browse files
authored
Merge pull request #340 from topcoder-platform/feat/GAME-131
assign member selection to badge via CSV
2 parents a27297e + 9cddb7d commit 1a4198a

File tree

11 files changed

+180
-7
lines changed

11 files changed

+180
-7
lines changed

src-ts/lib/functions/xhr-functions/xhr.functions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ function interceptError(instance: AxiosInstance): void {
7474

7575
// if there is server error message, then return it inside `message` property of error
7676
error.message = error?.response?.data?.message || error.message
77+
// if there is server errors data, then return it inside `errors` property of error
78+
error.errors = error?.response?.data?.errors
7779

7880
return Promise.reject(error)
7981
}

src-ts/tools/gamification-admin/game-config/gamification-config.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface GamificationConfigModel {
22
ACCEPTED_BADGE_MIME_TYPES: string
3+
CSV_HEADER: Array<string>,
34
MAX_BADGE_IMAGE_FILE_SIZE: number
45
ORG_ID: string
56
PAGE_SIZE: number

src-ts/tools/gamification-admin/game-config/gamification.default.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { GamificationConfigModel } from './gamification-config.model'
22

33
export const GamificationConfigDefault: GamificationConfigModel = {
44
ACCEPTED_BADGE_MIME_TYPES: 'image/svg+xml,image/svg',
5+
CSV_HEADER: ['tc_handle', 'badge_id'],
56
MAX_BADGE_IMAGE_FILE_SIZE: 5000000, // 5mb in bytes
67
ORG_ID: '6052dd9b-ea80-494b-b258-edd1331e27a3',
78
PAGE_SIZE: 12,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@import "../../../../../lib/styles/variables";
2+
@import "../../../../../lib/styles/includes";
3+
4+
.wrapper {
5+
display: flex;
6+
flex-direction: column;
7+
justify-content: space-between;
8+
9+
.badge {
10+
display: flex;
11+
align-items: center;
12+
margin-bottom: $space-xxl;
13+
14+
@include ltemd {
15+
margin-bottom: 0;
16+
}
17+
18+
.badge-image {
19+
width: 43px;
20+
height: 43px;
21+
margin-right: $space-xl;
22+
}
23+
24+
.badge-image-disabled {
25+
width: 43px;
26+
height: 43px;
27+
margin-right: $space-xl;
28+
opacity: 0.5;
29+
filter: grayscale(1);
30+
}
31+
32+
.badge-name {
33+
font-size: 16px;
34+
}
35+
}
36+
37+
.actions-wrap {
38+
display: flex;
39+
flex-direction: column;
40+
41+
.actions {
42+
display: flex;
43+
align-items: center;
44+
45+
@include ltemd {
46+
justify-content: flex-end;
47+
}
48+
49+
a {
50+
margin-right: $space-md;
51+
}
52+
}
53+
}
54+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FC } from 'react'
2+
3+
import { BaseModal, Button, PageDivider, useCheckIsMobile } from '../../../../../lib'
4+
import { GameBadge } from '../../game-badge.model'
5+
6+
import styles from './BadgeAssignedModal.module.scss'
7+
export interface BadgeAssignedModalProps {
8+
badge: GameBadge
9+
isOpen: boolean
10+
onClose: () => void
11+
}
12+
13+
const BadgeAssignedModal: FC<BadgeAssignedModalProps> = (props: BadgeAssignedModalProps) => {
14+
15+
const isMobile: boolean = useCheckIsMobile()
16+
17+
function onClose(): void {
18+
props.onClose()
19+
}
20+
21+
return (
22+
<BaseModal
23+
onClose={onClose}
24+
open={props.isOpen}
25+
size='md'
26+
title={`Badge created`}
27+
closeOnOverlayClick={false}
28+
>
29+
<div className={styles.wrapper}>
30+
<div className={styles.badge}>
31+
<img
32+
alt={props.badge.badge_name}
33+
className={styles[props.badge.active ? 'badge-image' : 'badge-image-disabled']}
34+
src={props.badge.badge_image_url}
35+
/>
36+
<p className={styles['badge-name']}>{props.badge.badge_name} badge has been sucessfully awarded.</p>
37+
</div>
38+
<div className={styles['actions-wrap']}>
39+
{
40+
isMobile && <PageDivider />
41+
}
42+
<div className={styles.actions}>
43+
<Button
44+
label='Close'
45+
buttonStyle='primary'
46+
onClick={onClose}
47+
/>
48+
</div>
49+
</div>
50+
</div>
51+
</BaseModal>
52+
)
53+
}
54+
55+
export default BadgeAssignedModal
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as BadgeAssignedModal } from './BadgeAssignedModal'

src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { GameBadge } from '../../../game-lib'
55
import styles from './AwardedMembersTab.module.scss'
66

77
export interface AwardedMembersTabProps {
8-
awardedMembers?: GameBadge['member_badges']
8+
badge: GameBadge
99
}
1010

1111
const AwardedMembersTab: FC<AwardedMembersTabProps> = (props: AwardedMembersTabProps) => {

src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,11 @@ const BadgeDetailPage: FC = () => {
193193

194194
// default tab
195195
let activeTabElement: JSX.Element
196-
= <AwardedMembersTab
197-
awardedMembers={badgeDetailsHandler.data?.member_badges}
198-
/>
196+
= <AwardedMembersTab badge={badgeDetailsHandler.data as GameBadge} />
199197
if (activeTab === BadgeDetailsTabViews.manualAward) {
200-
activeTabElement = <ManualAwardTab awardedMembers={badgeDetailsHandler.data?.member_badges} />
198+
activeTabElement = <ManualAwardTab
199+
badge={badgeDetailsHandler.data as GameBadge}
200+
/>
201201
}
202202
if (activeTab === BadgeDetailsTabViews.batchAward) {
203203
activeTabElement = <BatchAwardTab />

src-ts/tools/gamification-admin/pages/badge-detail/ManualAwardTab/ManualAwardTab.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
1+
import { find } from 'lodash'
12
import { Dispatch, FC, SetStateAction, useState } from 'react'
23

34
import { Button } from '../../../../../lib'
45
import { InputHandleAutocomplete } from '../../../../../lib/member-autocomplete'
56
import { MembersAutocompeteResult } from '../../../../../lib/member-autocomplete/input-handle-functions'
67
import { GameBadge } from '../../../game-lib'
8+
import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal'
9+
import { generateCSV, manualAssignRequestAsync } from '../badge-details.functions'
710

811
import styles from './ManualAwardTab.module.scss'
912

1013
export interface ManualAwardTabProps {
11-
awardedMembers?: GameBadge['member_badges']
14+
badge: GameBadge
1215
}
1316

1417
const ManualAwardTab: FC<ManualAwardTabProps> = (props: ManualAwardTabProps) => {
1518

1619
const [selectedMembers, setSelectedMembers]: [Array<MembersAutocompeteResult>, Dispatch<SetStateAction<Array<MembersAutocompeteResult>>>]
1720
= useState<Array<MembersAutocompeteResult>>([])
1821

22+
const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)
23+
24+
const [badgeAssignError, setBadgeAssignError]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()
25+
1926
function onAward(): void {
20-
setSelectedMembers([])
27+
const csv: string = generateCSV(
28+
selectedMembers.map(m => [m.handle, props.badge?.id as string])
29+
)
30+
setBadgeAssignError(undefined)
31+
manualAssignRequestAsync(csv)
32+
.then(() => {
33+
setShowBadgeAssigned(true)
34+
setSelectedMembers([])
35+
})
36+
.catch(e => {
37+
let message: string = e.message
38+
if (e.errors && e.errors[0] && e.errors[0].path === 'user_id') {
39+
const handleOrId: string = find(selectedMembers, { userId: e.errors[0].value })?.handle || e.errors[0].value
40+
message = `Member ${handleOrId} alredy owns this badge.`
41+
}
42+
setBadgeAssignError(message)
43+
})
2144
}
2245

2346
return (
@@ -33,6 +56,8 @@ const ManualAwardTab: FC<ManualAwardTabProps> = (props: ManualAwardTabProps) =>
3356
onChange={setSelectedMembers}
3457
tabIndex={0}
3558
value={selectedMembers}
59+
error={badgeAssignError}
60+
dirty={!!badgeAssignError}
3661
/>
3762
<div className={styles.actionsWrap}>
3863
<Button
@@ -45,6 +70,15 @@ const ManualAwardTab: FC<ManualAwardTabProps> = (props: ManualAwardTabProps) =>
4570
</div>
4671
</div>
4772
</div>
73+
{
74+
showBadgeAssigned && <BadgeAssignedModal
75+
badge={props.badge}
76+
isOpen={showBadgeAssigned}
77+
onClose={() => {
78+
setShowBadgeAssigned(false)
79+
}}
80+
/>
81+
}
4882
</div>
4983
)
5084
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
import { GamificationConfig } from '../../game-config'
12
import { GameBadge } from '../../game-lib'
23

4+
import { submitRequestAsync as submitBadgeAssingRequestAsync } from './manual-assign-badge.store'
35
import { submitRequestAsync as submitBadgeUpdateRequestAsync } from './update-badge.store'
46
import { UpdateBadgeRequest } from './updated-badge-request.model'
57

68
export async function submitRequestAsync(request: UpdateBadgeRequest): Promise<GameBadge> {
79
return submitBadgeUpdateRequestAsync(request)
810
}
11+
12+
export function generateCSV(input: Array<Array<string | number>>): string {
13+
input.unshift(GamificationConfig.CSV_HEADER)
14+
15+
return input.map(row => row.join(',')).join('\n')
16+
}
17+
18+
export async function manualAssignRequestAsync(csv: string): Promise<any> {
19+
return submitBadgeAssingRequestAsync(csv)
20+
}

0 commit comments

Comments
 (0)