Skip to content

Commit b7b6a4e

Browse files
authored
Merge pull request #348 from topcoder-platform/GAME-QA-S4
Game qa s4
2 parents 3095557 + 6a96e84 commit b7b6a4e

File tree

8 files changed

+122
-15
lines changed

8 files changed

+122
-15
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"redux-logger": "^3.0.6",
6262
"redux-promise-middleware": "^6.1.2",
6363
"redux-thunk": "^2.4.1",
64+
"sanitize-html": "^2.7.2",
6465
"sass": "^1.49.8",
6566
"styled-components": "^5.3.5",
6667
"swr": "^1.3.0",
@@ -97,6 +98,7 @@
9798
"@types/react-gtm-module": "^2.0.1",
9899
"@types/react-redux-toastr": "^7.6.2",
99100
"@types/react-router-dom": "^5.3.3",
101+
"@types/sanitize-html": "^2.6.2",
100102
"@types/segment-analytics": "^0.0.34",
101103
"@types/systemjs": "^6.1.0",
102104
"@types/uuid": "^8.3.4",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { KeyedMutator } from 'swr'
2+
3+
import { InfinitePageDao } from './infinite-page-dao.model'
4+
15
export interface InfinitePageHandler<T> {
26
data?: ReadonlyArray<T>
37
getAndSetNext: () => void
48
hasMore: boolean
9+
mutate: KeyedMutator<Array<InfinitePageDao<T>>>
510
}

src-ts/lib/pagination/use-infinite-page.hook.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { InfinitePageHandler } from './infinite-page-handler.model'
88
export function useGetInfinitePage<T>(getKey: (index: number, previousPageData: InfinitePageDao<T>) => string | undefined):
99
InfinitePageHandler<T> {
1010

11-
const { data, setSize, size }: SWRInfiniteResponse<InfinitePageDao<T>> = useSWRInfinite(getKey, { revalidateFirstPage: false })
11+
const { data, mutate, setSize, size }: SWRInfiniteResponse<InfinitePageDao<T>> = useSWRInfinite(getKey, { revalidateFirstPage: false })
1212

1313
// flatten version of badges paginated data
1414
const outputData: ReadonlyArray<T> = flatten(map(data, dao => dao.rows))
@@ -21,5 +21,6 @@ export function useGetInfinitePage<T>(getKey: (index: number, previousPageData:
2121
data: outputData,
2222
getAndSetNext,
2323
hasMore: outputData.length < (data?.[0]?.count || 0),
24+
mutate,
2425
}
2526
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@import "../../../../lib/styles/variables";
22
@import "../../../../lib/styles/includes";
3+
@import "../../../../lib/styles/typography";
4+
$error-line-height: 14px;
35

46
$badgePreview: 130px;
57
$badgePreviewImage: 72px;
@@ -86,6 +88,22 @@ $badgePreviewImage: 72px;
8688
}
8789
}
8890

91+
.error {
92+
display: flex;
93+
align-items: center;
94+
color: $red-100;
95+
// extend body ultra small and override it
96+
@extend .ultra-small;
97+
line-height: $error-line-height;
98+
margin-top: $space-xs;
99+
100+
svg {
101+
@include icon-md;
102+
fill: $red-100;
103+
margin-right: $space-xs;
104+
}
105+
}
106+
89107
.badgeDesc {
90108
margin-top: $space-sm;
91109

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

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { noop, trim } from 'lodash'
22
import MarkdownIt from 'markdown-it'
3-
import { createRef, Dispatch, FC, KeyboardEvent, RefObject, SetStateAction, useEffect, useState } from 'react'
3+
import { ChangeEvent, createRef, Dispatch, FC, KeyboardEvent, RefObject, SetStateAction, useEffect, useState } from 'react'
44
import ContentEditable from 'react-contenteditable'
55
import { Params, useLocation, useParams } from 'react-router-dom'
66
import { toast } from 'react-toastify'
7+
import sanitizeHtml from 'sanitize-html'
8+
import { KeyedMutator } from 'swr'
79

8-
import { Breadcrumb, BreadcrumbItemModel, Button, ButtonProps, ContentLayout, IconOutline, LoadingSpinner, PageDivider, TabsNavbar, TabsNavItem } from '../../../../lib'
10+
import { Breadcrumb, BreadcrumbItemModel, Button, ButtonProps, ContentLayout, IconOutline, IconSolid, LoadingSpinner, PageDivider, Sort, tableGetDefaultSort, TabsNavbar, TabsNavItem } from '../../../../lib'
911
import { GamificationConfig } from '../../game-config'
10-
import { BadgeDetailPageHandler, GameBadge, useGamificationBreadcrumb, useGetGameBadgeDetails } from '../../game-lib'
11-
import { BadgeActivatedModal } from '../../game-lib/modals/badge-activated-modal'
12+
import { BadgeDetailPageHandler, GameBadge, useGamificationBreadcrumb, useGetGameBadgeDetails, useGetGameBadgesPage } from '../../game-lib'
13+
import { badgeListingColumns } from '../badge-listing/badge-listing-table'
1214

1315
import AwardedMembersTab from './AwardedMembersTab/AwardedMembersTab'
1416
import { badgeDetailsTabs, BadgeDetailsTabViews } from './badge-details-tabs.config'
@@ -68,7 +70,11 @@ const BadgeDetailPage: FC = () => {
6870

6971
const [isBadgeDescEditingMode, setIsBadgeDescEditingMode]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)
7072

71-
const [showActivatedModal, setShowActivatedModal]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false)
73+
// badgeListingMutate will reset badge listing page cache when called
74+
const sort: Sort = tableGetDefaultSort(badgeListingColumns)
75+
const { mutate: badgeListingMutate }: { mutate: KeyedMutator<any> } = useGetGameBadgesPage(sort)
76+
77+
const [badgeNameErrorText, setBadgeNameErrorText]: [string | undefined, Dispatch<SetStateAction<string | undefined>>] = useState<string | undefined>()
7278

7379
useEffect(() => {
7480
if (newImageFile && newImageFile.length) {
@@ -100,6 +106,7 @@ const BadgeDetailPage: FC = () => {
100106
...badgeDetailsHandler.data,
101107
badge_image_url: updatedBadge.badge_image_url,
102108
})
109+
onBadgeUpdated()
103110
})
104111
}
105112
}, [
@@ -170,6 +177,9 @@ const BadgeDetailPage: FC = () => {
170177
if (e.key === 'Enter') {
171178
e.preventDefault()
172179
badgeNameRef.current?.blur()
180+
} else if (/[`'<>]+/.test(e.key)) {
181+
// restrict those characters
182+
e.preventDefault()
173183
}
174184
}
175185

@@ -179,8 +189,19 @@ const BadgeDetailPage: FC = () => {
179189
}
180190
}
181191

192+
function sanitazeBadgeName(innerHTML: string): string {
193+
const clean: string = sanitizeHtml(innerHTML, {
194+
allowedTags: [],
195+
})
196+
return trim(clean)
197+
}
198+
182199
function onSaveBadgeName(): any {
183-
const newBadgeName: string | undefined = trim(badgeNameRef.current?.innerHTML)
200+
const newBadgeName: string = sanitazeBadgeName(badgeNameRef.current?.innerHTML as string)
201+
if (!newBadgeName) {
202+
setBadgeNameErrorText('Update rejected due to invalid title string.')
203+
return
204+
}
184205
if (newBadgeName !== badgeDetailsHandler.data?.badge_name) {
185206
// save only if different
186207
updateBadgeAsync({
@@ -193,6 +214,10 @@ const BadgeDetailPage: FC = () => {
193214
...badgeDetailsHandler.data,
194215
badge_name: newBadgeName,
195216
})
217+
onBadgeUpdated()
218+
})
219+
.catch(e => {
220+
setBadgeNameErrorText(e.message)
196221
})
197222
}
198223
}
@@ -212,10 +237,25 @@ const BadgeDetailPage: FC = () => {
212237
...badgeDetailsHandler.data,
213238
badge_description: newBadgeDesc,
214239
})
240+
onBadgeUpdated()
215241
})
216242
}
217243
}
218244

245+
function onBadgeUpdated(): void {
246+
badgeListingMutate()
247+
}
248+
249+
function validateFilePicked(e: ChangeEvent<HTMLInputElement>): void {
250+
if (e.target.files?.length) {
251+
if (GamificationConfig.ACCEPTED_BADGE_MIME_TYPES.includes(e.target.files[0].type)) {
252+
setNewImageFile(e.target.files)
253+
} else {
254+
toast.error(`Not allowed file type: ${e.target.files[0].type}`)
255+
}
256+
}
257+
}
258+
219259
// default tab
220260
let activeTabElement: JSX.Element
221261
= <AwardedMembersTab badge={badgeDetailsHandler.data as GameBadge} />
@@ -261,19 +301,25 @@ const BadgeDetailPage: FC = () => {
261301
className={styles.filePickerInput}
262302
accept={GamificationConfig.ACCEPTED_BADGE_MIME_TYPES}
263303
size={GamificationConfig.MAX_BADGE_IMAGE_FILE_SIZE}
264-
onChange={e => setNewImageFile(e.target.files)}
304+
onChange={validateFilePicked}
265305
/>
266306
</div>
267307
<div className={styles.badgeDetails}>
268308
<ContentEditable
269309
innerRef={badgeNameRef}
270310
html={badgeDetailsHandler.data?.badge_name as string}
271-
onChange={noop}
311+
onChange={() => badgeNameErrorText ? setBadgeNameErrorText(undefined) : ''}
272312
onKeyDown={onNameEditKeyDown}
273313
onBlur={onSaveBadgeName}
274314
onFocus={onBadgeNameEditFocus}
275315
className={styles.badgeName}
276316
/>
317+
{
318+
badgeNameErrorText && <div className={styles.error}>
319+
<IconSolid.ExclamationIcon />
320+
{badgeNameErrorText}
321+
</div>
322+
}
277323
<div className={styles.badgeDesc}>
278324
<div className={styles.badgeEditWrap}>
279325
<ContentEditable

src-ts/tools/gamification-admin/pages/badge-listing/badge-listing-table/badge-action-renderer/BadgeActionRenderer.module.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
.badge-actions {
55
display: flex;
66
align-items: center;
7-
justify-content: center;
7+
justify-content: flex-end;
88
padding-top: $space-lg;
9+
padding-right: $space-sm;
910

1011
@include ltemd {
1112
flex-direction: column;

src-ts/tools/gamification-admin/pages/badge-listing/badge-listing-table/badge-action-renderer/BadgeActionRenderer.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ function BadgeActionRenderer(badge: GameBadge): JSX.Element {
2020
{
2121
label: 'View',
2222
},
23-
{
24-
label: 'Edit',
25-
view: 'edit',
26-
},
2723
{
2824
label: 'Award',
2925
view: 'award',

yarn.lock

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2740,6 +2740,13 @@
27402740
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
27412741
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
27422742

2743+
"@types/sanitize-html@^2.6.2":
2744+
version "2.6.2"
2745+
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9"
2746+
integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==
2747+
dependencies:
2748+
htmlparser2 "^6.0.0"
2749+
27432750
"@types/scheduler@*":
27442751
version "0.16.2"
27452752
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
@@ -8244,7 +8251,7 @@ html2canvas@^1.4.1:
82448251
css-line-break "^2.1.0"
82458252
text-segmentation "^1.0.3"
82468253

8247-
htmlparser2@^6.1.0:
8254+
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
82488255
version "6.1.0"
82498256
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
82508257
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
@@ -8842,6 +8849,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
88428849
dependencies:
88438850
isobject "^3.0.1"
88448851

8852+
is-plain-object@^5.0.0:
8853+
version "5.0.0"
8854+
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
8855+
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
8856+
88458857
is-port-reachable@4.0.0:
88468858
version "4.0.0"
88478859
resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d"
@@ -11656,6 +11668,11 @@ parse-passwd@^1.0.0:
1165611668
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
1165711669
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
1165811670

11671+
parse-srcset@^1.0.2:
11672+
version "1.0.2"
11673+
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
11674+
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
11675+
1165911676
parse5@5.1.0:
1166011677
version "5.1.0"
1166111678
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
@@ -12574,6 +12591,15 @@ postcss@^8.2.7, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.7:
1257412591
picocolors "^1.0.0"
1257512592
source-map-js "^1.0.2"
1257612593

12594+
postcss@^8.3.11:
12595+
version "8.4.18"
12596+
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2"
12597+
integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==
12598+
dependencies:
12599+
nanoid "^3.3.4"
12600+
picocolors "^1.0.0"
12601+
source-map-js "^1.0.2"
12602+
1257712603
prelude-ls@^1.2.1:
1257812604
version "1.2.1"
1257912605
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -13798,6 +13824,18 @@ sane@^4.0.3:
1379813824
minimist "^1.1.1"
1379913825
walker "~1.0.5"
1380013826

13827+
sanitize-html@^2.7.2:
13828+
version "2.7.2"
13829+
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.2.tgz#54c5189af75e3237d996e4b9a5e3eaad12c7f7fc"
13830+
integrity sha512-DggSTe7MviO+K4YTCwprG6W1vsG+IIX67yp/QY55yQqKCJYSWzCA1rZbaXzkjoKeL9+jqwm56wD6srYLtUNivg==
13831+
dependencies:
13832+
deepmerge "^4.2.2"
13833+
escape-string-regexp "^4.0.0"
13834+
htmlparser2 "^6.0.0"
13835+
is-plain-object "^5.0.0"
13836+
parse-srcset "^1.0.2"
13837+
postcss "^8.3.11"
13838+
1380113839
sanitize.css@*:
1380213840
version "13.0.0"
1380313841
resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173"

0 commit comments

Comments
 (0)