From 9ad4a80c44c412aa1f901a7198730ce7ee0bc4f6 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Fri, 21 Oct 2022 13:28:05 +0300 Subject: [PATCH 1/2] GAME-132 #comment generic file picker --- .../lib/form/form-groups/form-input/index.ts | 1 + .../InputFilePicker.module.scss | 31 ++++++++ .../input-file-picker/InputFilePicker.tsx | 74 +++++++++++++++++++ .../form-input/input-file-picker/index.ts | 1 + .../BatchAwardTab/BatchAwardTab.module.scss | 44 +++++++++++ .../BatchAwardTab/BatchAwardTab.tsx | 70 +++++++++++++++++- 6 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.module.scss create mode 100644 src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.tsx create mode 100644 src-ts/lib/form/form-groups/form-input/input-file-picker/index.ts diff --git a/src-ts/lib/form/form-groups/form-input/index.ts b/src-ts/lib/form/form-groups/form-input/index.ts index 4e43914cb..55de8809e 100644 --- a/src-ts/lib/form/form-groups/form-input/index.ts +++ b/src-ts/lib/form/form-groups/form-input/index.ts @@ -1,3 +1,4 @@ +export * from './input-file-picker' export * from './input-image-picker' export * from './form-input-autcomplete-option.enum' export * from './input-rating' diff --git a/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.module.scss b/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.module.scss new file mode 100644 index 000000000..4ee129767 --- /dev/null +++ b/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.module.scss @@ -0,0 +1,31 @@ +@import "../../../../styles/includes"; +@import "../../../../styles/variables"; + +.filePicker { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: $black-5; + border-radius: 4px; + width: 100%; + padding: $space-xxxxl; + position: relative; + + @include ltemd { + width: 100%; + } + + .fileName { + margin-bottom: $space-sm; + text-align: center; + } + + .filePickerButton { + color: $turq-160; + } + + .filePickerInput { + display: none; + } +} \ No newline at end of file diff --git a/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.tsx b/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.tsx new file mode 100644 index 000000000..0ff3ba70d --- /dev/null +++ b/src-ts/lib/form/form-groups/form-input/input-file-picker/InputFilePicker.tsx @@ -0,0 +1,74 @@ +// tslint:disable:no-null-keyword +import { createRef, Dispatch, FC, RefObject, SetStateAction, useEffect, useState } from 'react' + +import { Button, useCheckIsMobile } from '../../../..' +import { InputValue } from '../../../form-input.model' + +import styles from './InputFilePicker.module.scss' + +interface InputFilePickerProps { + readonly fileConfig?: { + readonly acceptFileType?: string + readonly maxFileSize?: number + } + readonly name: string + readonly onChange: (fileList: FileList | null) => void + readonly value?: InputValue +} + +const InputFilePicker: FC = (props: InputFilePickerProps) => { + + const isMobile: boolean = useCheckIsMobile() + + const fileInputRef: RefObject = createRef() + + const [files, setFiles]: [FileList | null, Dispatch>] = useState(null) + const [fileName, setFileName]: [string | undefined, Dispatch>] = useState() + + useEffect(() => { + if (files && files.length) { + setFileName(files[0].name) + } else if (fileName) { + setFileName(undefined) + } + }, [ + files, + fileName, + ]) + + return ( +
+ { + fileName &&

{fileName}

+ } +
+ ) +} + +export default InputFilePicker diff --git a/src-ts/lib/form/form-groups/form-input/input-file-picker/index.ts b/src-ts/lib/form/form-groups/form-input/input-file-picker/index.ts new file mode 100644 index 000000000..9a1a26f3c --- /dev/null +++ b/src-ts/lib/form/form-groups/form-input/input-file-picker/index.ts @@ -0,0 +1 @@ +export { default as InputFilePicker } from './InputFilePicker' diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss index 36bd71e18..87db83910 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss @@ -1,3 +1,47 @@ +@use '../../../../../lib/styles/typography'; +@import "../../../../../lib/styles/variables/palette"; +@import "../../../../../lib/styles/includes"; + +$error-line-height: 14px; + .tabWrap { display: flex; + flex-direction: column; + padding-bottom: 260px; + + .batchFormWrap { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $space-xxxxl; + margin-top: $space-xxl; + + @include ltemd { + grid-template-columns: 1fr; + } + + .batchForm { + display: flex; + flex-direction: column; + + .error { + display: flex; + align-items: center; + color: $red-100; + // extend body ultra small and override it + @extend .ultra-small; + line-height: $error-line-height; + margin-top: $space-xs; + + svg { + @include icon-md; + fill: $red-100; + margin-right: $space-xs; + } + } + + .actionsWrap { + margin-top: $space-xxl; + } + } + } } \ No newline at end of file diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx index d71def501..7f52ab5c6 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx @@ -1,11 +1,77 @@ -import { FC } from 'react' +import { Dispatch, FC, SetStateAction, useState } from 'react' + +import { Button, IconSolid, InputFilePicker } from '../../../../../lib' +import { GameBadge } from '../../../game-lib' +import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal' import styles from './BatchAwardTab.module.scss' +interface BatchAwardTabProps { + badge: GameBadge, + onBatchAssign: () => void +} + +const BatchAwardTab: FC = (props: BatchAwardTabProps) => { + + const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch>] = useState(false) + + // tslint:disable-next-line:no-null-keyword + const [files, setFiles]: [FileList | null, Dispatch>] = useState(null) + + const [errorText, setErrorText]: [string, Dispatch>] = useState('') + + function onFilePick(fileList: FileList | null): void { + if (fileList && fileList[0] && fileList[0].type !== 'text/csv') { + setErrorText('Only CSV files are allowed.') + } else { + setFiles(fileList) + setErrorText('') + } + } + + function onAward(): void { + + } -const BatchAwardTab: FC = () => { return (

Batch Award

+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Neque ullamcorper neque sed orci, enim amet, sed.

+
+ + {errorText && ( +
+ + {errorText} +
+ )} +
+
+
+
+ { + showBadgeAssigned && { + setShowBadgeAssigned(false) + props.onBatchAssign() + }} + /> + }
) } From 4990b962203e6174663e2add2101e6334a2f99cf Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 24 Oct 2022 10:42:55 +0300 Subject: [PATCH 2/2] GAME-133 #comment integrate batch assign with API --- public/gamification-admin/bulk.sample.csv | 4 ++++ .../AwardedMembersTab/AwardedMembersTab.tsx | 2 +- .../pages/badge-detail/BadgeDetailPage.tsx | 9 +++++--- .../BatchAwardTab/BatchAwardTab.module.scss | 8 +++++++ .../BatchAwardTab/BatchAwardTab.tsx | 21 ++++++++++++++++--- .../badge-detail/badge-details.functions.ts | 5 +++++ .../badge-detail/batch-assign-badge.store.ts | 17 +++++++++++++++ 7 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 public/gamification-admin/bulk.sample.csv create mode 100644 src-ts/tools/gamification-admin/pages/badge-detail/batch-assign-badge.store.ts diff --git a/public/gamification-admin/bulk.sample.csv b/public/gamification-admin/bulk.sample.csv new file mode 100644 index 000000000..e09c450ed --- /dev/null +++ b/public/gamification-admin/bulk.sample.csv @@ -0,0 +1,4 @@ +tc_handle,badge_id,badge_name,awarded_by,awarded_at +kirildev,858dafad-5fcd-4bc3-ab7c-849453d139ad,,kirildev,2022-08-15T07:25:43.187Z +jcori,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,,2022-08-15T07:25:43.187Z +amy_admin,bc39b152-e0e3-4984-962b-f1eba67229dd,TCO22 Development Finalist,, diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx index c1a24147d..f781167b0 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.tsx @@ -24,7 +24,7 @@ const AwardedMembersTab: FC = (props: AwardedMembersTabP const pageHandler: InfinitePageHandler = useGetGameBadgeAssigneesPage(props.badge, sort) useEffect(() => { - if (props.forceRefresh && pageHandler) { + if (props.forceRefresh && pageHandler && !pageHandler.isValidating) { pageHandler.mutate() } }, [ diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx index 9b7961833..88ed03921 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx @@ -264,7 +264,7 @@ const BadgeDetailPage: FC = () => { } } - function onManualAssign(): void { + function onAssign(): void { // refresh awardedMembers data setForceAwardedMembersTabRefresh(true) setActiveTab(BadgeDetailsTabViews.awardedMembers) @@ -279,11 +279,14 @@ const BadgeDetailPage: FC = () => { if (activeTab === BadgeDetailsTabViews.manualAward) { activeTabElement = } if (activeTab === BadgeDetailsTabViews.batchAward) { - activeTabElement = + activeTabElement = } // show page loader if we fetching results diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss index 87db83910..61beede7d 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.module.scss @@ -19,6 +19,14 @@ $error-line-height: 14px; grid-template-columns: 1fr; } + .templateLink { + text-transform: uppercase; + color: $turq-160; + font-weight: $font-weight-bold; + margin-top: $space-lg; + display: inline-block; + } + .batchForm { display: flex; flex-direction: column; diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx index 7f52ab5c6..3e9e9998e 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BatchAwardTab/BatchAwardTab.tsx @@ -1,8 +1,10 @@ +// tslint:disable:no-null-keyword import { Dispatch, FC, SetStateAction, useState } from 'react' import { Button, IconSolid, InputFilePicker } from '../../../../../lib' import { GameBadge } from '../../../game-lib' import { BadgeAssignedModal } from '../../../game-lib/modals/badge-assigned-modal' +import { batchAssignRequestAsync } from '../badge-details.functions' import styles from './BatchAwardTab.module.scss' interface BatchAwardTabProps { @@ -14,7 +16,6 @@ const BatchAwardTab: FC = (props: BatchAwardTabProps) => { const [showBadgeAssigned, setShowBadgeAssigned]: [boolean, Dispatch>] = useState(false) - // tslint:disable-next-line:no-null-keyword const [files, setFiles]: [FileList | null, Dispatch>] = useState(null) const [errorText, setErrorText]: [string, Dispatch>] = useState('') @@ -29,14 +30,28 @@ const BatchAwardTab: FC = (props: BatchAwardTabProps) => { } function onAward(): void { - + batchAssignRequestAsync(files?.item(0) as File) + .then(() => { + setShowBadgeAssigned(true) + setFiles(null) + }) + .catch(e => { + let message: string = e.message + if (e.errors && e.errors[0] && e.errors[0].path === 'user_id') { + message = `CSV file contains duplicate data. There are members included already owning this badge.` + } + setErrorText(message) + }) } return (

Batch Award

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Neque ullamcorper neque sed orci, enim amet, sed.

+
+

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.

+ Download template CSV +
>): string { export async function manualAssignRequestAsync(csv: string): Promise { return submitBadgeAssingRequestAsync(csv) } + +export async function batchAssignRequestAsync(batchFile: File): Promise { + return submitBatchAssignRequestAsync(batchFile) +} diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/batch-assign-badge.store.ts b/src-ts/tools/gamification-admin/pages/badge-detail/batch-assign-badge.store.ts new file mode 100644 index 000000000..f15b0c58d --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/batch-assign-badge.store.ts @@ -0,0 +1,17 @@ +import { EnvironmentConfig } from '../../../../config' +import { xhrPostAsync } from '../../../../lib' + +export async function submitRequestAsync(batchFile: File): Promise { + const url: string = `${EnvironmentConfig.API.V5}/gamification/badges/assign` + + const form: any = new FormData() + + // fill the form + form.append('file', batchFile) + + return xhrPostAsync(url, form, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) +}