Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"redux-thunk": "^2.4.1",
"sass": "^1.49.8",
"styled-components": "^5.3.5",
"swr": "^1.3.0",
"tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.4",
"typescript": "^4.6.3",
"uuid": "^8.3.2"
Expand Down
3 changes: 3 additions & 0 deletions src-ts/lib/global-config.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export interface GlobalConfig {
}
DISABLED_TOOLS?: Array<string>
ENV: string
GAMIFICATION: {
ORG_ID: string
},
LOGGING: {
PUBLIC_TOKEN: string
SERVICE: string
Expand Down
3 changes: 3 additions & 0 deletions src-ts/lib/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './infinite-page-dao.model'
export * from './infinite-page-handler.model'
export * from './page.model'
export * from './sort.model'
export * from './use-infinite-page.hook'
5 changes: 5 additions & 0 deletions src-ts/lib/pagination/infinite-page-dao.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface InfinitePageDao<T> {
count: number
// TODO: rename this 'items' so it can be used in a grid/card view
rows: ReadonlyArray<T>
}
5 changes: 5 additions & 0 deletions src-ts/lib/pagination/infinite-page-handler.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface InfinitePageHandler<T> {
data?: ReadonlyArray<T>
getAndSetNext: () => void
hasMore: boolean
}
25 changes: 25 additions & 0 deletions src-ts/lib/pagination/use-infinite-page.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { flatten, map } from 'lodash'
// tslint:disable-next-line: no-submodule-imports
import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite'

import { InfinitePageDao } from './infinite-page-dao.model'
import { InfinitePageHandler } from './infinite-page-handler.model'

export function useGetInfinitePage<T>(getKey: (index: number, previousPageData: InfinitePageDao<T>) => string | undefined):
InfinitePageHandler<T> {

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

// flatten version of badges paginated data
const outputData: ReadonlyArray<T> = flatten(map(data, dao => dao.rows))

function getAndSetNext(): void {
setSize(size + 1)
}

return {
data: outputData,
getAndSetNext,
hasMore: outputData.length < (data?.[0]?.count || 0),
}
}
9 changes: 9 additions & 0 deletions src-ts/lib/table/Table.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
margin-right: -29px;
}
}

&.centerHeader {
justify-content: center;
}
}

.tooltip {
Expand Down Expand Up @@ -90,3 +94,8 @@
.tootlipBody {
min-width: 200px;
}

.loadBtnWrap {
display: flex;
justify-content: center;
}
29 changes: 26 additions & 3 deletions src-ts/lib/table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import classNames from 'classnames'
import { Dispatch, MouseEvent, SetStateAction, useEffect, useState } from 'react'

import { Button } from '../button'
import { Sort } from '../pagination'
import '../styles/_includes.scss'
import { IconOutline } from '../svgs'
Expand All @@ -15,7 +16,10 @@ import styles from './Table.module.scss'
interface TableProps<T> {
readonly columns: ReadonlyArray<TableColumn<T>>
readonly data: ReadonlyArray<T>
readonly moreToLoad?: boolean
readonly onLoadMoreClick?: () => void
readonly onRowClick?: (data: T) => void
readonly onToggleSort?: (sort: Sort) => void
}

interface DefaultSortDirectionMap {
Expand All @@ -34,7 +38,7 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
Dispatch<SetStateAction<DefaultSortDirectionMap | undefined>>
]
= useState<DefaultSortDirectionMap | undefined>()
const [sortedData, setSortData]: [ReadonlyArray<T>, Dispatch<SetStateAction<ReadonlyArray<T>>>]
const [sortedData, setSortedData]: [ReadonlyArray<T>, Dispatch<SetStateAction<ReadonlyArray<T>>>]
= useState<ReadonlyArray<T>>(props.data)

useEffect(() => {
Expand All @@ -47,7 +51,11 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
setDefaultSortDirectionMap(map)
}

setSortData(tableGetSorted(data, columns, sort))
// if we have a sort handler, don't worry about getting the sorted data;
// otherwise, get the sorted data for the table
const sorted: ReadonlyArray<T> = !!props.onToggleSort ? data : tableGetSorted(data, columns, sort)

setSortedData(sorted)
},
[
columns,
Expand Down Expand Up @@ -75,6 +83,9 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
fieldName,
}
setSort(newSort)

// call the callback to notify parent for sort update
props.onToggleSort?.(newSort)
}

const headerRow: Array<JSX.Element> = props.columns
Expand Down Expand Up @@ -136,7 +147,7 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
// return the entire row
return (
<tr
className={classNames(styles.tr, !!onRowClick ? styles.clickable : undefined)}
className={classNames(styles.tr, props.onRowClick ? styles.clickable : undefined)}
onClick={onRowClick}
key={index}
>
Expand All @@ -158,6 +169,18 @@ const Table: <T extends { [propertyName: string]: any }>(props: TableProps<T>) =
{rowCells}
</tbody>
</table>
{
!!props.moreToLoad && !!props.onLoadMoreClick && (
<div className={styles['loadBtnWrap']}>
<Button
buttonStyle='tertiary'
label='Load More'
size='lg'
onClick={props.onLoadMoreClick}
/>
</div>
)
}
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions src-ts/lib/table/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './table-column.model'
export { tableGetDefaultSort } from './table-functions'
export { default as Table } from './Table'
18 changes: 11 additions & 7 deletions src-ts/lib/table/table-functions/table.functions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Sort } from '../../pagination'
import { TableColumn } from '../table-column.model'

export function getDefaultSort<T>(columns: ReadonlyArray<TableColumn<T>>): Sort | undefined {
export function getDefaultSort<T>(columns: ReadonlyArray<TableColumn<T>>): Sort {

const defaultSortColumn: TableColumn<T> | undefined = columns.find(col => col.isDefaultSort)
|| columns.find(col => !!col.propertyName)
|| columns?.[0]

const defaultSort: Sort | undefined = !defaultSortColumn?.propertyName
? undefined
: {
direction: defaultSortColumn.defaultSortDirection || 'asc',
fieldName: defaultSortColumn.propertyName,
}
// if we didn't find a default sort, we have a problem
if (!defaultSortColumn) {
throw new Error('A table must have at least one column.')
}

const defaultSort: Sort = {
direction: defaultSortColumn.defaultSortDirection || 'asc',
fieldName: defaultSortColumn.propertyName || '',
}

return defaultSort
}
Expand Down
11 changes: 9 additions & 2 deletions src-ts/tools/gamification-admin/GamificationAdmin.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FC, useContext } from 'react'
import { Outlet, Routes } from 'react-router-dom'
import { SWRConfig } from 'swr'

import {
routeContext,
RouteContextData,
xhrGetAsync,
} from '../../lib'

export const toolTitle: string = 'Gamification Admin'
Expand All @@ -13,12 +15,17 @@ const GamificationAdmin: FC<{}> = () => {
const { getChildRoutes }: RouteContextData = useContext(routeContext)

return (
<>
<SWRConfig
value={{
fetcher: (resource) => xhrGetAsync(resource),
refreshInterval: 60000, // 1 min
}}
>
<Outlet />
<Routes>
{getChildRoutes(toolTitle)}
</Routes>
</>
</SWRConfig>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface GamificationConfigModel {
ORG_ID: string
PAGE_SIZE: number
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { EnvironmentConfig } from '../../../config'

import { GamificationConfigModel } from './gamification-config.model'
import { GamificationConfigDefault } from './gamification.default.config'
import { GamificationConfigDev } from './gamification.dev.config'
import { GamificationConfigProd } from './gamification.prod.config'

function getConfig(): GamificationConfigModel {

switch (EnvironmentConfig.ENV) {

case 'dev':
return GamificationConfigDev

case 'prod':
return GamificationConfigProd

default:
return GamificationConfigDefault
}
}

const GamificationConfig: GamificationConfigModel = {
...getConfig(),
}

export default GamificationConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GamificationConfigModel } from './gamification-config.model'

export const GamificationConfigDefault: GamificationConfigModel = {
ORG_ID: '6052dd9b-ea80-494b-b258-edd1331e27a3',
PAGE_SIZE: 12,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GamificationConfigModel } from './gamification-config.model'
import { GamificationConfigDefault } from './gamification.default.config'

export const GamificationConfigDev: GamificationConfigModel = {
...GamificationConfigDefault,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GamificationConfigModel } from './gamification-config.model'
import { GamificationConfigDefault } from './gamification.default.config'

export const GamificationConfigProd: GamificationConfigModel = {
...GamificationConfigDefault,
ORG_ID: 'e111f8df-6ac8-44d1-b4da-bb916f5e3425',
}
1 change: 1 addition & 0 deletions src-ts/tools/gamification-admin/game-config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as GamificationConfig } from './gamification.config'
10 changes: 10 additions & 0 deletions src-ts/tools/gamification-admin/game-lib/game-badge.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// TODO: add factory to convert snake case property names to camel case
export interface GameBadge {
active: boolean
badge_description: string
badge_image_url: string
badge_name: string
badge_status: string
id: string
organization_id: string
}
3 changes: 3 additions & 0 deletions src-ts/tools/gamification-admin/game-lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './game-badge.model'
export * from './use-get-game-badges-page.hook'
export * from './use-gamification-breadcrumb.hook'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BreadcrumbItemModel } from '../../../lib'
import { basePath } from '../gamification-admin.routes'
import { toolTitle } from '../GamificationAdmin'

export function useGamificationBreadcrumb(items: Array<BreadcrumbItemModel>): Array<BreadcrumbItemModel> {

const breadcrumb: Array<BreadcrumbItemModel> = [
{
name: toolTitle,
url: basePath,
},
...items,
]

return breadcrumb
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { EnvironmentConfig } from '../../../config'
import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../lib'
import { GamificationConfig } from '../game-config'

import { GameBadge } from './game-badge.model'

export function useGetGameBadgesPage(sort: Sort): InfinitePageHandler<GameBadge> {

function getKey(index: number, previousPageData: InfinitePageDao<GameBadge>): string | undefined {

// reached the end
if (!!previousPageData && !previousPageData.rows.length) {
return undefined
}

const params: Record<string, string> = {
limit: `${GamificationConfig.PAGE_SIZE}`,
offset: `${index * GamificationConfig.PAGE_SIZE}`,
order_by: sort.fieldName,
order_type: sort.direction,
organization_id: GamificationConfig.ORG_ID,
}

const badgeEndpointUrl: URL = new URL(`${EnvironmentConfig.API.V5}/gamification/badges?${new URLSearchParams(params)}`)

return badgeEndpointUrl.toString()
}

return useGetInfinitePage(getKey)
}
22 changes: 16 additions & 6 deletions src-ts/tools/gamification-admin/gamification-admin.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ import BadgeDetailPage from './pages/badge-detail/BadgeDetailPage'
import BadgeListingPage from './pages/badge-listing/BadgeListingPage'
import CreateBadgePage from './pages/create-badge/CreateBadgePage'

export const baseUrl: string = '/gamification-admin'
export const rolesRequired: Array<string> = [UserRole.gamificationAdmin]
const baseDetailPath: string = '/badge-detail'
const createBadgePath: string = '/create-badge'

export const basePath: string = '/gamification-admin'

export function badgeDetailPath(badgeId: string, view?: 'edit' | 'award'): string {
return `${basePath}${baseDetailPath}/${badgeId}${!!view ? `#${view}` : ''}`
}

export const createBadgeRoute: string = `${basePath}${createBadgePath}`

export const gamificationAdminRoutes: Array<PlatformRoute> = [
{
Expand All @@ -18,17 +26,19 @@ export const gamificationAdminRoutes: Array<PlatformRoute> = [
},
{
element: <CreateBadgePage />,
route: '/create-badge',
route: createBadgePath,
},
{
element: <BadgeDetailPage />,
route: '/badge-detail',
route: `${baseDetailPath}/:id`,
},
],
element: <GamificationAdmin />,
hidden: true,
rolesRequired,
route: baseUrl,
rolesRequired: [
UserRole.gamificationAdmin,
],
route: basePath,
title: toolTitle,
},
]
Loading