diff --git a/src-ts/lib/pagination/infinite-page-dao.model.ts b/src-ts/lib/pagination/infinite-page-dao.model.ts index ae1ca01da..4be8d2556 100644 --- a/src-ts/lib/pagination/infinite-page-dao.model.ts +++ b/src-ts/lib/pagination/infinite-page-dao.model.ts @@ -1,5 +1,7 @@ export interface InfinitePageDao { count: number + limit: number + offset: number // TODO: rename this 'items' so it can be used in a grid/card view rows: ReadonlyArray } diff --git a/src-ts/tools/gamification-admin/game-lib/game-badge.model.ts b/src-ts/tools/gamification-admin/game-lib/game-badge.model.ts index f4de99dd3..e60ae3091 100644 --- a/src-ts/tools/gamification-admin/game-lib/game-badge.model.ts +++ b/src-ts/tools/gamification-admin/game-lib/game-badge.model.ts @@ -1,16 +1,18 @@ // TODO: add factory to convert snake case property names to camel case +export interface MemberBadgeAward { + awarded_at: string + awarded_by: string + user_handle: string + user_id: string +} + export interface GameBadge { - active: boolean - badge_description: string - badge_image_url: string - badge_name: string - badge_status: string - id: string - member_badges?: Array<{ - awarded_at: string, - awarded_by: string, - user_handle: string, - user_id: string, - }> - organization_id: string + active: boolean + badge_description: string + badge_image_url: string + badge_name: string + badge_status: string + id: string + member_badges?: Array + organization_id: string } diff --git a/src-ts/tools/gamification-admin/game-lib/use-gamification-breadcrumb.hook.tsx b/src-ts/tools/gamification-admin/game-lib/hooks/use-gamification-breadcrumb.hook.tsx similarity index 63% rename from src-ts/tools/gamification-admin/game-lib/use-gamification-breadcrumb.hook.tsx rename to src-ts/tools/gamification-admin/game-lib/hooks/use-gamification-breadcrumb.hook.tsx index 274022b16..fedd9236a 100644 --- a/src-ts/tools/gamification-admin/game-lib/use-gamification-breadcrumb.hook.tsx +++ b/src-ts/tools/gamification-admin/game-lib/hooks/use-gamification-breadcrumb.hook.tsx @@ -1,6 +1,6 @@ -import { BreadcrumbItemModel } from '../../../lib' -import { basePath } from '../gamification-admin.routes' -import { toolTitle } from '../GamificationAdmin' +import { BreadcrumbItemModel } from '../../../../lib' +import { basePath } from '../../gamification-admin.routes' +import { toolTitle } from '../../GamificationAdmin' export function useGamificationBreadcrumb(items: Array): Array { diff --git a/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-assignees-page.hook.ts b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-assignees-page.hook.ts new file mode 100644 index 000000000..099c69d78 --- /dev/null +++ b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-assignees-page.hook.ts @@ -0,0 +1,28 @@ +import { EnvironmentConfig } from '../../../../config' +import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../../lib' +import { GamificationConfig } from '../../game-config' +import { GameBadge, MemberBadgeAward } from '../game-badge.model' + +export function useGetGameBadgeAssigneesPage(badge: GameBadge, sort: Sort): InfinitePageHandler { + + function getKey(index: number, previousPageData: InfinitePageDao): string | undefined { + + // reached the end + if (!!previousPageData && !previousPageData.rows.length) { + return undefined + } + + const params: Record = { + limit: `${GamificationConfig.PAGE_SIZE}`, + offset: `${index * GamificationConfig.PAGE_SIZE}`, + order_by: sort.fieldName, + order_type: sort.direction, + } + + const badgeEndpointUrl: URL = new URL(`${EnvironmentConfig.API.V5}/gamification/badges/${badge.id}/assignees?${new URLSearchParams(params)}`) + + return badgeEndpointUrl.toString() + } + + return useGetInfinitePage(getKey) +} diff --git a/src-ts/tools/gamification-admin/game-lib/use-get-game-badge-details.hook.ts b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-details.hook.ts similarity index 83% rename from src-ts/tools/gamification-admin/game-lib/use-get-game-badge-details.hook.ts rename to src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-details.hook.ts index 349cf1efa..2fd56b170 100644 --- a/src-ts/tools/gamification-admin/game-lib/use-get-game-badge-details.hook.ts +++ b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badge-details.hook.ts @@ -1,8 +1,7 @@ import useSWR, { KeyedMutator, SWRResponse } from 'swr' -import { EnvironmentConfig } from '../../../config' - -import { GameBadge } from './game-badge.model' +import { EnvironmentConfig } from '../../../../config' +import { GameBadge } from '../game-badge.model' export interface BadgeDetailPageHandler { data?: Readonly diff --git a/src-ts/tools/gamification-admin/game-lib/use-get-game-badges-page.hook.ts b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badges-page.hook.ts similarity index 82% rename from src-ts/tools/gamification-admin/game-lib/use-get-game-badges-page.hook.ts rename to src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badges-page.hook.ts index 885d1af51..dbe5e46a0 100644 --- a/src-ts/tools/gamification-admin/game-lib/use-get-game-badges-page.hook.ts +++ b/src-ts/tools/gamification-admin/game-lib/hooks/use-get-game-badges-page.hook.ts @@ -1,8 +1,7 @@ -import { EnvironmentConfig } from '../../../config' -import { InfinitePageDao, InfinitePageHandler, Sort, useGetInfinitePage } from '../../../lib' -import { GamificationConfig } from '../game-config' - -import { GameBadge } from './game-badge.model' +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 { diff --git a/src-ts/tools/gamification-admin/game-lib/index.ts b/src-ts/tools/gamification-admin/game-lib/index.ts index 9c270733b..17bcc6e2c 100644 --- a/src-ts/tools/gamification-admin/game-lib/index.ts +++ b/src-ts/tools/gamification-admin/game-lib/index.ts @@ -1,4 +1,4 @@ export * from './game-badge.model' -export * from './use-get-game-badges-page.hook' -export * from './use-gamification-breadcrumb.hook' -export * from './use-get-game-badge-details.hook' +export * from './hooks/use-get-game-badges-page.hook' +export * from './hooks/use-gamification-breadcrumb.hook' +export * from './hooks/use-get-game-badge-details.hook' diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.module.scss index 36bd71e18..7c67ed5bf 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.module.scss +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/AwardedMembersTab.module.scss @@ -1,3 +1,6 @@ +@import "../../../../../lib/styles/variables/spacing"; + .tabWrap { display: flex; + padding-bottom: $space-xxxxl; } \ No newline at end of file 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 eff8fe2ff..c1a24147d 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 @@ -1,17 +1,54 @@ -import { FC } from 'react' +import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' -import { GameBadge } from '../../../game-lib' +import { InfinitePageHandler, Sort, Table, TableColumn, tableGetDefaultSort } from '../../../../../lib' +import { GameBadge, MemberBadgeAward } from '../../../game-lib' +import { useGetGameBadgeAssigneesPage } from '../../../game-lib/hooks/use-get-game-badge-assignees-page.hook' +import { awardedMembersColumns } from './awarded-members-table/awarded-members-table.config' import styles from './AwardedMembersTab.module.scss' export interface AwardedMembersTabProps { badge: GameBadge + forceRefresh?: boolean } const AwardedMembersTab: FC = (props: AwardedMembersTabProps) => { + const [sort, setSort]: [Sort, Dispatch>] = useState(tableGetDefaultSort(awardedMembersColumns)) + + const [columns]: [ + ReadonlyArray>, + Dispatch>>>, + ] + = useState>>([...awardedMembersColumns]) + + const pageHandler: InfinitePageHandler = useGetGameBadgeAssigneesPage(props.badge, sort) + + useEffect(() => { + if (props.forceRefresh && pageHandler) { + pageHandler.mutate() + } + }, [ + props.forceRefresh, + pageHandler, + ]) + + function onSortClick(newSort: Sort): void { + setSort({ ...newSort }) + } + return (
- + { + pageHandler.data?.length ? ( + + ) : undefined + } ) } diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/awarded-members-table.config.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/awarded-members-table.config.tsx new file mode 100644 index 000000000..741104373 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/awarded-members-table.config.tsx @@ -0,0 +1,28 @@ +import { TableColumn } from '../../../../../../lib' +import { MemberBadgeAward } from '../../../../game-lib' + +import { MemberActionRenderer } from './member-action-renderer' +import { MemberAwaredAtRenderer } from './member-awardedAt-renderer' +import { MemberHandleRenderer } from './member-handle-renderer' + +export const awardedMembersColumns: ReadonlyArray> = [ + { + defaultSortDirection: 'asc', + isDefaultSort: true, + label: 'Handle', + propertyName: 'user_handle', + renderer: MemberHandleRenderer, + type: 'element', + }, + { + defaultSortDirection: 'asc', + label: 'Awarded at', + propertyName: 'awarded_at', + renderer: MemberAwaredAtRenderer, + type: 'element', + }, + { + renderer: MemberActionRenderer, + type: 'action', + }, +] diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.module.scss new file mode 100644 index 000000000..2bad46878 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.module.scss @@ -0,0 +1,28 @@ +@import "../../../../../../../lib/styles/includes"; +@import "../../../../../../../lib/styles/variables"; + +.badge-actions { + display: flex; + align-items: center; + justify-content: flex-end; + padding-top: $space-lg; + padding-right: $space-sm; + + @include ltemd { + flex-direction: column; + align-items: flex-end; + } + + a { + margin-right: $space-sm; + + @include ltemd { + margin-right: 0; + margin-bottom: $space-sm; + } + + &:last-child { + margin-right: 0; + } + } +} \ No newline at end of file diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.tsx new file mode 100644 index 000000000..d39f75048 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/MemberActionRenderer.tsx @@ -0,0 +1,43 @@ +import { Button, ButtonProps, useCheckIsMobile } from '../../../../../../../lib' +import { MemberBadgeAward } from '../../../../../game-lib' + +import styles from './MemberActionRenderer.module.scss' + +function MemberActionRenderer(memberAward: MemberBadgeAward): JSX.Element { + + const isMobile: boolean = useCheckIsMobile() + + const buttonProps: ButtonProps = { + buttonStyle: 'secondary', + size: isMobile ? 'xs' : 'sm', + } + + const actionButtons: Array<{ + label: string + }> = [ + { + label: 'Unassign', + }, + ] + + function onUnassign(): void { + // TODO: unassign feature + } + + return ( +
+ {actionButtons.map((button, index) => { + return ( +
+ ) +} + +export default MemberActionRenderer diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/index.ts b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/index.ts new file mode 100644 index 000000000..9208d82c8 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-action-renderer/index.ts @@ -0,0 +1 @@ +export { default as MemberActionRenderer } from './MemberActionRenderer' diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.module.scss new file mode 100644 index 000000000..464301847 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.module.scss @@ -0,0 +1,12 @@ +@import "../../../../../../../lib/styles/variables"; +@import "../../../../../../../lib/styles/includes"; + +.memberAwardedAt { + font-size: 16px; + font-weight: $font-weight-normal; + text-align: left; + + @include ltemd { + font-size: 14px; + } +} \ No newline at end of file diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.tsx new file mode 100644 index 000000000..fa9263cce --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/MemberAwaredAtRenderer.tsx @@ -0,0 +1,19 @@ +import { MemberBadgeAward } from '../../../../../game-lib' + +import styles from './MemberAwaredAtRenderer.module.scss' + +function MemberAwaredAtRenderer(memberAward: MemberBadgeAward): JSX.Element { + const dateFormat: Record = { + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + month: 'short', + year: 'numeric', + } + + return ( +
{new Date(memberAward.awarded_at).toLocaleString(undefined, dateFormat)}
+ ) +} + +export default MemberAwaredAtRenderer diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/index.ts b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/index.ts new file mode 100644 index 000000000..542529e7b --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-awardedAt-renderer/index.ts @@ -0,0 +1 @@ +export { default as MemberAwaredAtRenderer } from './MemberAwaredAtRenderer' diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.module.scss b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.module.scss new file mode 100644 index 000000000..4e335a08f --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.module.scss @@ -0,0 +1,22 @@ +@import "../../../../../../../lib/styles/includes"; +@import "../../../../../../../lib/styles/variables"; + +.memberAward { + display: flex; + align-items: center; + + .memberHandle { + font-size: 16px; + font-weight: 700; + line-height: 24px; + color: $black-100; + } + + .profileLink { + cursor: pointer; + color: $turq-160; + width: 16px; + height: 16px; + margin-left: $space-xs; + } +} \ No newline at end of file diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.tsx b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.tsx new file mode 100644 index 000000000..5f6458809 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/MemberHandleRenderer.tsx @@ -0,0 +1,19 @@ +import { EnvironmentConfig } from '../../../../../../../config' +import { IconOutline } from '../../../../../../../lib' +import { MemberBadgeAward } from '../../../../../game-lib' + +import styles from './MemberHandleRenderer.module.scss' + +function MemberHandleRenderer(memberAward: MemberBadgeAward): JSX.Element { + return ( +
+

{memberAward.user_handle}

+ window.open(`${EnvironmentConfig.TOPCODER_URLS.USER_PROFILE}/${memberAward.user_handle}`, '_blank')} + /> +
+ ) +} + +export default MemberHandleRenderer diff --git a/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/index.ts b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/index.ts new file mode 100644 index 000000000..35655ae76 --- /dev/null +++ b/src-ts/tools/gamification-admin/pages/badge-detail/AwardedMembersTab/awarded-members-table/member-handle-renderer/index.ts @@ -0,0 +1 @@ +export { default as MemberHandleRenderer } from './MemberHandleRenderer' 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 c3148a3ce..9b7961833 100644 --- a/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx +++ b/src-ts/tools/gamification-admin/pages/badge-detail/BadgeDetailPage.tsx @@ -79,6 +79,9 @@ const BadgeDetailPage: FC = () => { const [showActivatedModal, setShowActivatedModal]: [boolean, Dispatch>] = useState(false) + const [forceAwardedMembersTabRefresh, setForceAwardedMembersTabRefresh]: [boolean | undefined, Dispatch>] + = useState() + useEffect(() => { if (newImageFile && newImageFile.length) { const fileReader: FileReader = new FileReader() @@ -261,13 +264,22 @@ const BadgeDetailPage: FC = () => { } } + function onManualAssign(): void { + // refresh awardedMembers data + setForceAwardedMembersTabRefresh(true) + setActiveTab(BadgeDetailsTabViews.awardedMembers) + } + // default tab let activeTabElement: JSX.Element - = + = if (activeTab === BadgeDetailsTabViews.manualAward) { activeTabElement = setActiveTab(BadgeDetailsTabViews.awardedMembers)} + onManualAssign={onManualAssign} /> } if (activeTab === BadgeDetailsTabViews.batchAward) {