Skip to content

Commit 65520c0

Browse files
authored
Merge pull request #378 from topcoder-platform/feat/GAME-125
GAME-125 batch assign from CSV -> gamification
2 parents badc964 + 4990b96 commit 65520c0

File tree

11 files changed

+275
-6
lines changed

11 files changed

+275
-6
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
tc_handle,badge_id,badge_name,awarded_by,awarded_at
2+
kirildev,858dafad-5fcd-4bc3-ab7c-849453d139ad,,kirildev,2022-08-15T07:25:43.187Z
3+
jcori,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,,2022-08-15T07:25:43.187Z
4+
amy_admin,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,,

src-ts/lib/form/form-groups/form-input/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './input-file-picker'
12
export * from './input-image-picker'
23
export * from './form-input-autcomplete-option.enum'
34
export * from './input-rating'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@import "../../../../styles/includes";
2+
@import "../../../../styles/variables";
3+
4+
.filePicker {
5+
display: flex;
6+
flex-direction: column;
7+
align-items: center;
8+
justify-content: center;
9+
background-color: $black-5;
10+
border-radius: 4px;
11+
width: 100%;
12+
padding: $space-xxxxl;
13+
position: relative;
14+
15+
@include ltemd {
16+
width: 100%;
17+
}
18+
19+
.fileName {
20+
margin-bottom: $space-sm;
21+
text-align: center;
22+
}
23+
24+
.filePickerButton {
25+
color: $turq-160;
26+
}
27+
28+
.filePickerInput {
29+
display: none;
30+
}
31+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// tslint:disable:no-null-keyword
2+
import { createRef, Dispatch, FC, RefObject, SetStateAction, useEffect, useState } from 'react'
3+
4+
import { Button, useCheckIsMobile } from '../../../..'
5+
import { InputValue } from '../../../form-input.model'
6+
7+
import styles from './InputFilePicker.module.scss'
8+
9+
interface InputFilePickerProps {
10+
readonly fileConfig?: {
11+
readonly acceptFileType?: string
12+
readonly maxFileSize?: number
13+
}
14+
readonly name: string
15+
readonly onChange: (fileList: FileList | null) => void
16+
readonly value?: InputValue
17+
}
18+
19+
const InputFilePicker: FC<InputFilePickerProps> = (props: InputFilePickerProps) => {
20+
21+
const isMobile: boolean = useCheckIsMobile()
22+
23+
const fileInputRef: RefObject<HTMLInputElement> = createRef<HTMLInputElement>()
24+
25+
const [files, setFiles]: [FileList | null, Dispatch<SetStateAction<FileList | null>>] = useState<FileList | null>(null)
26+
const [fileName, setFileName]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()
27+
28+
useEffect(() => {
29+
if (files && files.length) {
30+
setFileName(files[0].name)
31+
} else if (fileName) {
32+
setFileName(undefined)
33+
}
34+
}, [
35+
files,
36+
fileName,
37+
])
38+
39+
return (
40+
<div className={styles.filePicker}>
41+
{
42+
fileName && <p className={styles.fileName}>{fileName}</p>
43+
}
44+
<Button
45+
buttonStyle='secondary'
46+
className={styles.filePickerButton}
47+
label={fileName ? 'Clear' : 'Browse'}
48+
onClick={() => {
49+
if (fileName) {
50+
setFiles(null)
51+
props.onChange(null)
52+
} else {
53+
fileInputRef.current?.click()
54+
}
55+
}}
56+
size={isMobile ? 'xs' : 'sm'}
57+
/>
58+
<input
59+
name={props.name}
60+
type={'file'}
61+
accept={props.fileConfig?.acceptFileType || '*'}
62+
className={styles.filePickerInput}
63+
ref={fileInputRef}
64+
onChange={event => {
65+
setFiles(event.target.files)
66+
props.onChange(event.target.files)
67+
}}
68+
size={props.fileConfig?.maxFileSize || Infinity}
69+
/>
70+
</div>
71+
)
72+
}
73+
74+
export default InputFilePicker
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as InputFilePicker } from './InputFilePicker'

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
@@ -24,7 +24,7 @@ const AwardedMembersTab: FC<AwardedMembersTabProps> = (props: AwardedMembersTabP
2424
const pageHandler: InfinitePageHandler<MemberBadgeAward> = useGetGameBadgeAssigneesPage(props.badge, sort)
2525

2626
useEffect(() => {
27-
if (props.forceRefresh && pageHandler) {
27+
if (props.forceRefresh && pageHandler && !pageHandler.isValidating) {
2828
pageHandler.mutate()
2929
}
3030
}, [

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ const BadgeDetailPage: FC = () => {
264264
}
265265
}
266266

267-
function onManualAssign(): void {
267+
function onAssign(): void {
268268
// refresh awardedMembers data
269269
setForceAwardedMembersTabRefresh(true)
270270
setActiveTab(BadgeDetailsTabViews.awardedMembers)
@@ -279,11 +279,14 @@ const BadgeDetailPage: FC = () => {
279279
if (activeTab === BadgeDetailsTabViews.manualAward) {
280280
activeTabElement = <ManualAwardTab
281281
badge={badgeDetailsHandler.data as GameBadge}
282-
onManualAssign={onManualAssign}
282+
onManualAssign={onAssign}
283283
/>
284284
}
285285
if (activeTab === BadgeDetailsTabViews.batchAward) {
286-
activeTabElement = <BatchAwardTab />
286+
activeTabElement = <BatchAwardTab
287+
badge={badgeDetailsHandler.data as GameBadge}
288+
onBatchAssign={onAssign}
289+
/>
287290
}
288291

289292
// show page loader if we fetching results
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
@use '../../../../../lib/styles/typography';
2+
@import "../../../../../lib/styles/variables/palette";
3+
@import "../../../../../lib/styles/includes";
4+
5+
$error-line-height: 14px;
6+
17
.tabWrap {
28
display: flex;
9+
flex-direction: column;
10+
padding-bottom: 260px;
11+
12+
.batchFormWrap {
13+
display: grid;
14+
grid-template-columns: 1fr 1fr;
15+
gap: $space-xxxxl;
16+
margin-top: $space-xxl;
17+
18+
@include ltemd {
19+
grid-template-columns: 1fr;
20+
}
21+
22+
.templateLink {
23+
text-transform: uppercase;
24+
color: $turq-160;
25+
font-weight: $font-weight-bold;
26+
margin-top: $space-lg;
27+
display: inline-block;
28+
}
29+
30+
.batchForm {
31+
display: flex;
32+
flex-direction: column;
33+
34+
.error {
35+
display: flex;
36+
align-items: center;
37+
color: $red-100;
38+
// extend body ultra small and override it
39+
@extend .ultra-small;
40+
line-height: $error-line-height;
41+
margin-top: $space-xs;
42+
43+
svg {
44+
@include icon-md;
45+
fill: $red-100;
46+
margin-right: $space-xs;
47+
}
48+
}
49+
50+
.actionsWrap {
51+
margin-top: $space-xxl;
52+
}
53+
}
54+
}
355
}

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

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,92 @@
1-
import { FC } from 'react'
1+
// tslint:disable:no-null-keyword
2+
import { Dispatch, FC, SetStateAction, useState } from 'react'
3+
4+
import { Button, IconSolid, InputFilePicker } from '../../../../../lib'
5+
import { GameBadge } from '../../../game-lib'
6+
import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal'
7+
import { batchAssignRequestAsync } from '../badge-details.functions'
28

39
import styles from './BatchAwardTab.module.scss'
10+
interface BatchAwardTabProps {
11+
badge: GameBadge,
12+
onBatchAssign: () => void
13+
}
14+
15+
const BatchAwardTab: FC<BatchAwardTabProps> = (props: BatchAwardTabProps) => {
16+
17+
const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)
18+
19+
const [files, setFiles]: [FileList | null, Dispatch<SetStateAction<FileList | null>>] = useState<FileList | null>(null)
20+
21+
const [errorText, setErrorText]: [string, Dispatch<SetStateAction<string>>] = useState<string>('')
22+
23+
function onFilePick(fileList: FileList | null): void {
24+
if (fileList && fileList[0] && fileList[0].type !== 'text/csv') {
25+
setErrorText('Only CSV files are allowed.')
26+
} else {
27+
setFiles(fileList)
28+
setErrorText('')
29+
}
30+
}
31+
32+
function onAward(): void {
33+
batchAssignRequestAsync(files?.item(0) as File)
34+
.then(() => {
35+
setShowBadgeAssigned(true)
36+
setFiles(null)
37+
})
38+
.catch(e => {
39+
let message: string = e.message
40+
if (e.errors && e.errors[0] && e.errors[0].path === 'user_id') {
41+
message = `CSV file contains duplicate data. There are members included already owning this badge.`
42+
}
43+
setErrorText(message)
44+
})
45+
}
446

5-
const BatchAwardTab: FC = () => {
647
return (
748
<div className={styles.tabWrap}>
849
<h3>Batch Award</h3>
50+
<div className={styles.batchFormWrap}>
51+
<div>
52+
<p>If you would like to assign multiple people to multiple badges, this area is for you. Download the template below, populate the file with your data, and upload that file to the right once completed.</p>
53+
<a target={'_blank'} href='/gamification-admin/bulk.sample.csv' download='bulk.smaple.csv' className={styles.templateLink}>Download template CSV</a>
54+
</div>
55+
<div className={styles.batchForm}>
56+
<InputFilePicker
57+
fileConfig={{
58+
acceptFileType: 'text/csv',
59+
}}
60+
name='batch-import-file'
61+
onChange={onFilePick}
62+
/>
63+
{errorText && (
64+
<div className={styles.error}>
65+
<IconSolid.ExclamationIcon />
66+
{errorText}
67+
</div>
68+
)}
69+
<div className={styles.actionsWrap}>
70+
<Button
71+
buttonStyle='secondary'
72+
label='Award'
73+
className={styles.awardBtn}
74+
disable={!files?.length}
75+
onClick={onAward}
76+
/>
77+
</div>
78+
</div>
79+
</div>
80+
{
81+
showBadgeAssigned && <BadgeAssignedModal
82+
badge={props.badge}
83+
isOpen={showBadgeAssigned}
84+
onClose={() => {
85+
setShowBadgeAssigned(false)
86+
props.onBatchAssign()
87+
}}
88+
/>
89+
}
990
</div>
1091
)
1192
}

src-ts/tools/gamification-admin/pages/badge-detail/badge-details.functions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GamificationConfig } from '../../game-config'
22
import { GameBadge } from '../../game-lib'
33

4+
import { submitRequestAsync as submitBatchAssignRequestAsync } from './batch-assign-badge.store'
45
import { submitRequestAsync as submitBadgeAssingRequestAsync } from './manual-assign-badge.store'
56
import { submitRequestAsync as submitBadgeUpdateRequestAsync } from './update-badge.store'
67
import { UpdateBadgeRequest } from './updated-badge-request.model'
@@ -18,3 +19,7 @@ export function generateCSV(input: Array<Array<string | number>>): string {
1819
export async function manualAssignRequestAsync(csv: string): Promise<any> {
1920
return submitBadgeAssingRequestAsync(csv)
2021
}
22+
23+
export async function batchAssignRequestAsync(batchFile: File): Promise<any> {
24+
return submitBatchAssignRequestAsync(batchFile)
25+
}

0 commit comments

Comments
 (0)