From 340803991ddc98acd7f47320c291d4684d31a236 Mon Sep 17 00:00:00 2001 From: Valery <57412523+valerydluski@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:32:00 +0200 Subject: [PATCH 1/3] fix: update useCourseContext (#2309) * fix: update useCourseContext * fix: tests * chore: add unit tests * fix: prettier --- client/src/components/Header.tsx | 10 ++- client/src/components/PageLayout.tsx | 9 +-- .../Course/contexts/ActiveCourseContext.tsx | 41 ++++++++---- .../Course/contexts/SessionContext.test.tsx | 66 +++++++++++++++++++ .../src/modules/Home/pages/HomePage/index.tsx | 24 ++++--- .../MentorDashboard/MentorDashboard.test.tsx | 58 +++++++++++----- 6 files changed, 159 insertions(+), 49 deletions(-) create mode 100644 client/src/modules/Course/contexts/SessionContext.test.tsx diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index add1ef955d..5f9ea46a6c 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -9,7 +9,6 @@ import { SolutionOutlined, NotificationOutlined, } from '@ant-design/icons'; -import { Course } from 'services/models'; import { GithubAvatar } from 'components/GithubAvatar'; import { SolidarityUkraine } from './SolidarityUkraine'; import { SessionContext } from 'modules/Course/contexts'; @@ -19,7 +18,6 @@ import { useActiveCourseContext } from 'modules/Course/contexts/ActiveCourseCont type Props = { showCourseName?: boolean; title?: string; - course?: Course; }; const MENU_ITEMS = [ @@ -45,13 +43,13 @@ const MENU_ITEMS = [ }, ]; -export function Header({ title, showCourseName, course }: Props) { +export function Header({ title, showCourseName }: Props) { const { asPath: currentRoute } = useRouter(); const menuActiveItemStyle = { backgroundColor: '#e0f2ff' }; const session = useContext(SessionContext); - const activeCourse = useActiveCourseContext().course ?? course; - const courseLinks = useMemo(() => getNavigationItems(session, activeCourse ?? null), [course]); + const { course } = useActiveCourseContext(); + const courseLinks = useMemo(() => getNavigationItems(session, course ?? null), [course]); const menu = ( @@ -96,7 +94,7 @@ export function Header({ title, showCourseName, course }: Props) {
- {title} {showCourseName ? activeCourse?.name : null} + {title} {showCourseName ? course?.name : null}
diff --git a/client/src/components/PageLayout.tsx b/client/src/components/PageLayout.tsx index ede1a5e010..8868afea20 100644 --- a/client/src/components/PageLayout.tsx +++ b/client/src/components/PageLayout.tsx @@ -13,7 +13,6 @@ type Props = { noData?: boolean; background?: string; withMargin?: boolean; - course?: Course; }; export function PageLayout(props: Props) { @@ -22,7 +21,7 @@ export function PageLayout(props: Props) { return ( -
+
{props.error ? ( -
+
{props.noData ? (
no data
@@ -81,11 +80,9 @@ export function AdminPageLayout({ courses: Course[]; styles?: React.CSSProperties; }>) { - const [course] = courses; - return ( -
+
diff --git a/client/src/modules/Course/contexts/ActiveCourseContext.tsx b/client/src/modules/Course/contexts/ActiveCourseContext.tsx index c63dcac166..b7b53d3b6b 100644 --- a/client/src/modules/Course/contexts/ActiveCourseContext.tsx +++ b/client/src/modules/Course/contexts/ActiveCourseContext.tsx @@ -1,15 +1,23 @@ import { ProfileCourseDto } from 'api'; import { LoadingScreen } from 'components/LoadingScreen'; import { useRouter } from 'next/router'; -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useAsync, useLocalStorage } from 'react-use'; import { UserService } from 'services/user'; import { WelcomeCard } from 'components/WelcomeCard'; import { Alert, Col, Row } from 'antd'; -const ActiveCourseContext = React.createContext<{ course: ProfileCourseDto; courses: ProfileCourseDto[] }>( - {} as { course: ProfileCourseDto; courses: ProfileCourseDto[] }, -); +type ActiveCourseContextType = { + course: ProfileCourseDto; + courses: ProfileCourseDto[]; + setCourse: (course: ProfileCourseDto) => void; +}; + +const ActiveCourseContext = React.createContext({ + course: {} as ProfileCourseDto, + courses: [], + setCourse: () => {}, +}); export const useActiveCourseContext = () => { return useContext(ActiveCourseContext); @@ -23,12 +31,9 @@ export const ActiveCourseProvider = ({ children }: Props) => { const router = useRouter(); const alias = router.query.course; const [storageCourseId] = useLocalStorage('activeCourseId'); + const [activeCourse, setActiveCourse] = useState(); - const { - value: course, - error, - loading, - } = useAsync(async () => { + const { error, loading } = useAsync(async () => { if (!coursesCache) { coursesCache = await new UserService().getCourses(); } @@ -37,9 +42,17 @@ export const ActiveCourseProvider = ({ children }: Props) => { coursesCache.find(course => course.alias === alias) ?? coursesCache.find(course => course.id === storageCourseId) ?? coursesCache[0]; + + setActiveCourse(course); + return course; }, []); + const setCourse = (course: ProfileCourseDto) => { + setActiveCourse(course); + localStorage.setItem('activeCourseId', course.id.toString()); + }; + useEffect(() => { if (!error) { return; @@ -49,11 +62,11 @@ export const ActiveCourseProvider = ({ children }: Props) => { router.push('/login', { pathname: '/login', query: { url: redirectUrl } }); }, [error]); - if (!loading && !course && !coursesCache?.length) { + if (!loading && !activeCourse && !coursesCache?.length) { return ; } - if (alias && course && course.alias !== alias) { + if (alias && activeCourse && activeCourse.alias !== alias) { return ( @@ -67,9 +80,11 @@ export const ActiveCourseProvider = ({ children }: Props) => { ); } - if (course && coursesCache) { + if (activeCourse && coursesCache) { return ( - {children} + + {children} + ); } diff --git a/client/src/modules/Course/contexts/SessionContext.test.tsx b/client/src/modules/Course/contexts/SessionContext.test.tsx new file mode 100644 index 0000000000..be074ab0c0 --- /dev/null +++ b/client/src/modules/Course/contexts/SessionContext.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { SessionProvider } from './'; +import Router from 'next/router'; +import { useAsync } from 'react-use'; +import { useActiveCourseContext } from './ActiveCourseContext'; + +jest.mock('axios'); +jest.mock('next/router', () => ({ push: jest.fn() })); +jest.mock('./ActiveCourseContext', () => ({ + useActiveCourseContext: jest.fn(), +})); +jest.mock('react-use', () => ({ + useAsync: jest.fn(), +})); + +describe('', () => { + const mockChildren =
Child Component
; + + const mockSession = { isAdmin: true, courses: { 1: { roles: ['student'] } } }; + const mockCourse = { id: 1 }; + const mockActiveCourse = { course: mockCourse }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeEach(() => { + (useActiveCourseContext as jest.Mock).mockReturnValue(mockActiveCourse); + }); + + it('should render loading screen', () => { + (useAsync as jest.Mock).mockReturnValue({ loading: true }); + render({mockChildren}); + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + }); + + it('should handle error and redirect to login', () => { + (useAsync as jest.Mock).mockReturnValue({ error: true }); + render({mockChildren}); + expect(Router.push).toHaveBeenCalledWith('/login', expect.anything()); + }); + + it('should render children for admin user for admin-only pages', () => { + (useAsync as jest.Mock).mockReturnValue({ value: mockSession }); + render({mockChildren}); + expect(screen.getByText('Child Component')).toBeInTheDocument(); + }); + + it('should render warning for non-admin user for admin-only pages', () => { + (useAsync as jest.Mock).mockReturnValue({ value: { ...mockSession, isAdmin: false } }); + render({mockChildren}); + expect(screen.getByText(/You don't have required role to access this page/)).toBeInTheDocument(); + }); + + it('should render children for user with allowed roles', () => { + (useAsync as jest.Mock).mockReturnValue({ value: mockSession }); + render({mockChildren}); + expect(screen.getByText('Child Component')).toBeInTheDocument(); + }); + + it('should render warning for user without allowed roles', () => { + (useAsync as jest.Mock).mockReturnValue({ value: { ...mockSession, isAdmin: false } }); + render({mockChildren}); + expect(screen.getByText(/You don't have required role to access this page/)).toBeInTheDocument(); + }); +}); diff --git a/client/src/modules/Home/pages/HomePage/index.tsx b/client/src/modules/Home/pages/HomePage/index.tsx index 44f544f17b..394b6d5d2d 100644 --- a/client/src/modules/Home/pages/HomePage/index.tsx +++ b/client/src/modules/Home/pages/HomePage/index.tsx @@ -10,7 +10,6 @@ import { CourseSelector } from 'modules/Home/components/CourseSelector'; import { RegistryBanner } from 'modules/Home/components/RegistryBanner'; import { SystemAlerts } from 'modules/Home/components/SystemAlerts'; import { getCourseLinks } from 'modules/Home/data/links'; -import { useActiveCourse } from 'modules/Home/hooks/useActiveCourse'; import { useStudentSummary } from 'modules/Home/hooks/useStudentSummary'; import Link from 'next/link'; import { useMemo, useState, useContext } from 'react'; @@ -26,7 +25,7 @@ const mentorRegistryService = new MentorRegistryService(); const alertService = new AlertsApi(); export function HomePage() { - const { courses = [] } = useActiveCourseContext(); + const { courses = [], setCourse, course } = useActiveCourseContext(); const session = useContext(SessionContext); const plannedCourses = (courses || []).filter(course => course.planned && !course.inviteOnly); const wasMentor = isAnyMentor(session); @@ -34,13 +33,11 @@ export function HomePage() { wasMentor && plannedCourses.length > 0 && plannedCourses.every(course => session.courses[course.id] == null); const isPowerUser = isAnyCoursePowerUser(session) || isAnyCourseDementor(session); - - const [activeCourse, saveActiveCourseId] = useActiveCourse(courses); const [allCourses, setAllCourses] = useState([]); const [preselectedCourses, setPreselectedCourses] = useState([]); const [alerts, setAlerts] = useState([]); - const courseLinks = useMemo(() => getCourseLinks(session, activeCourse), [activeCourse]); + const courseLinks = useMemo(() => getCourseLinks(session, course), [course]); const [approvedCourse] = preselectedCourses.filter(course => !session.courses?.[course.id]); useAsync(async () => { @@ -59,15 +56,24 @@ export function HomePage() { setPreselectedCourses(preselectedCourses); }); - const { courseTasks, studentSummary } = useStudentSummary(session, activeCourse); + const handleChangeCourse = (courseId: number) => { + const course = courses.find(course => { + return course.id === courseId; + }); + if (course) { + setCourse(course); + } + }; + + const { courseTasks, studentSummary } = useStudentSummary(session, course); return (
- {isPowerUser && } + {isPowerUser && } - {!activeCourse && } + {!course && } {approvedCourse && (
@@ -88,7 +94,7 @@ export function HomePage() { {hasRegistryBanner && } - + diff --git a/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx b/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx index 1b7c26a693..e0b83464b5 100644 --- a/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx +++ b/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx @@ -4,6 +4,7 @@ import { Course } from 'services/models'; import { CourseInfo, Session } from 'components/withSession'; import { INSTRUCTIONS_TEXT } from '../Instructions'; import { MentorDashboardProps } from 'pages/course/mentor/dashboard'; +import { SessionContext } from 'modules/Course/contexts'; jest.mock('modules/Mentor/hooks/useMentorDashboard', () => ({ useMentorDashboard: jest.fn().mockReturnValue([[], false]), @@ -13,19 +14,6 @@ jest.mock('next/router', () => ({ })); const PROPS_MOCK: MentorDashboardProps = { - session: { - id: 1, - isActivist: false, - isAdmin: true, - isHirer: false, - githubId: 'github-id', - courses: { - '400': { - mentorId: 1, - roles: ['mentor'], - } as CourseInfo, - }, - } as Session, course: { id: 400, } as Course, @@ -36,7 +24,27 @@ const PROPS_MOCK: MentorDashboardProps = { describe('MentorDashboard', () => { it('should render instructions when mentor has no students for this course', () => { - render(); + render( + + + , + ); const instructionsTitle = screen.getByText(INSTRUCTIONS_TEXT.title); @@ -44,7 +52,27 @@ describe('MentorDashboard', () => { }); it('should render empty table when mentor has students for this course', () => { - render(); + render( + + + , + ); const emptyTable = screen.getByText(/No Data/i); From 39e6af1e1256b26580366b85f45df71b487fb0c1 Mon Sep 17 00:00:00 2001 From: Valery <57412523+valerydluski@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:28:27 +0200 Subject: [PATCH 2/3] fix: update mobile schedule view (#2306) * fix: update mobile schedule view * remove old code * fix: update header * fix: prettier --- client/src/components/Header.tsx | 35 ++++++------------- .../MobileItemCard/MobileItemCard.tsx | 13 +++---- .../Schedule/pages/SchedulePage/index.tsx | 28 ++++++++------- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 5f9ea46a6c..32cef51f16 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { Fragment, useContext, useMemo } from 'react'; -import { Button, Dropdown, Menu, Space, Tooltip } from 'antd'; +import { Button, Dropdown, Menu, Space } from 'antd'; import { EyeOutlined, LogoutOutlined, @@ -36,6 +36,12 @@ const MENU_ITEMS = [ icon: , title: 'My CV', }, + { + link: 'https://docs.app.rs.school', + icon: , + title: 'Help', + target: '_blank', + }, { link: '/api/v2/auth/github/logout', icon: , @@ -53,11 +59,11 @@ export function Header({ title, showCourseName }: Props) { const menu = ( - {MENU_ITEMS.map(({ link, icon, title }, id, arr) => ( + {MENU_ITEMS.map(({ link, icon, title, target }, id, arr) => ( {id === arr.length - 1 ? : null} - @@ -97,30 +103,15 @@ export function Header({ title, showCourseName }: Props) { {title} {showCourseName ? course?.name : null}
diff --git a/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx b/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx index 9f864c78c7..509b454bf3 100644 --- a/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx +++ b/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx @@ -3,21 +3,22 @@ import { Row, Col, Typography } from 'antd'; import { SwapRightOutlined } from '@ant-design/icons'; import { coloredDateRenderer } from 'components/Table'; import { renderTagWithStyle, statusRenderer } from '../TableView/renderers'; +import Link from 'next/link'; const { Title } = Typography; export const MobileItemCard = ({ item }: { item: CourseScheduleItemDto }) => { return ( -
- +
+ - - {item.name} - + + {item.name} + {renderTagWithStyle(item.tag)} - + {statusRenderer(item.status)} {coloredDateRenderer('UTC', 'MMM D HH:mm', 'start', 'Recommended date for studying')(item.startDate, item)} diff --git a/client/src/modules/Schedule/pages/SchedulePage/index.tsx b/client/src/modules/Schedule/pages/SchedulePage/index.tsx index ca9da59913..19fa634acc 100644 --- a/client/src/modules/Schedule/pages/SchedulePage/index.tsx +++ b/client/src/modules/Schedule/pages/SchedulePage/index.tsx @@ -87,19 +87,21 @@ export function SchedulePage() { <> - setCopyModal({})} - isCourseManager={isManager} - courseId={course.id} - courseAlias={course.alias} - settings={settings} - calendarToken={cipher} - tags={eventTags} - refreshData={refreshData} - mobileView={mobileView} - /> + {!mobileView && ( + setCopyModal({})} + isCourseManager={isManager} + courseId={course.id} + courseAlias={course.alias} + settings={settings} + calendarToken={cipher} + tags={eventTags} + refreshData={refreshData} + mobileView={mobileView} + /> + )} From e5de929166d864874a5e093a6050d9e8bd356227 Mon Sep 17 00:00:00 2001 From: Vadzim Antonau Date: Wed, 27 Sep 2023 21:03:55 +0300 Subject: [PATCH 3/3] feat(heroes): implement heroes radar (#2280) * feat(gratitudes): add heroes/radar endpoint to controller and service * feat(gratitudes): add first and last name to heroes-radar response * feat(gratitudes): add HeroesRadarQueryDto to correctly validate optional query param * feat(gratitudes): add ApiPropertyOptional to HeroesRadarQueryDto * feat(api): update openapi * feat(heroes): move heroes route to folder * feat(heroes): add blank heroes radar page * feat(gratitudes): add dtos to have response types in client api * feat(api): update openapi with dtos * fix(gratitudes): fields naming returned from DB * feat(heroes): move getFullName function to utils from component * feat(heroes-radar): add data fetching and basic test layout * feat(client): add HeroesRadarCard component * feat(client): add HeroesCountBadge component * refactor(client): update PublicFeedbackCard with HeroesCountBade * feat(client): add GithubAvatar to HeroesRadarCard * feat(client): add link to profile and total badges count at the bottom HeroesRadarCard * refactor(client): move HeroesCountBadge and HeroesRadarCard to components * feat(client): add course selection from to heroes radar and logic for it * feat(nestjs): add pagination to Heroes Radar, create and update dtos * feat(nestjs): update openapi * feat(client): update Heroes Radar for new API with pagination * feat(client): add Pagination to Heroes Radar * feat(client): replace Mansory with Row in Heroes Radar * feat(client): replace HeroesRadarCard with HeroesRadarTable * feat(client): process pagination with courseId in Heroes Radar * feat(client): add badges column width, get rid of some magic numbers * refactor(heroes-radar): remove console.log * fix(heroes-radar): used typeorm methods for pagination, optimize user info query * feat(heroes-radar): add notActivist query, refactor getHeroesRadar service method * feat(heroes-radar): update openapi * feat(heroes-radar): add notActivist checkbox to form * feat(heroes-radar): merge courseId and notActivist to one state * feat(heroes-radar): move name definition to dto from client * fix(heroes-radar): formatting * fix(heroes-radar): misspelling in variable name * refactor(profile): move missing key for HeroesCountBadge in PublicFeedbackCard * fix(heroes): remove redundant indent * fix(heroes-radar): setting parameter in sub query * chore(setup): add feedback (gratitudes) seeds * fix(heroes-radar): add try-finally in getHeroes * refactor(heroes-radar): replace string concatenation with template string * refactor(heroes-radar): get rid of redundant prefix in HeroesRadarBadgeDto * refactor(heroes): add typing to heroesBadges, remove any type assertion * refactor: move getFullName helper function to domain/user * fix(heroes-radar): remove unused deps in useCallback hooks * refactor(heroes-radar): fix formatting * feat(heroes-radar): move rank logic to backend, make ranking like in score page * refactor(gratitudes): replace with explicit conversion to number * refactor(heroes-rardar): replace div with Space component * refactor(heroes-radar): move onChange logic to parent component * refactor(heroes-radar): rename property items to heroes, add HeroesRadar interface * feat(heroes-radar): use pagination meta to calculate rank * feat(heroes-radar): add align start to Space * feat(heroes-radar): add additional ordering by github * feat(heroes-radar): remove equal total equal rank logic * refactor(heroes-radar): change submit button label * refactor(heroes): make tabs at Heroes page, move Heroes Radar to HeroesRadarTab * fix(heroes-radar): wrong import path * feat(heroes): replace deprecated TabPane * refactor(heroes): move tabs to separate variable * refactor(heroes): remove commented code * refactor(heroes-radar): replace Select.Option, remove margin left for clear button * refactor(profile): add badgesCount typing, replace keys with entries * refactor(heroes-radar): remove redundant Promise.all * feat(heroes-radar): get rid of redundant makeRequest --- client/src/api/api.ts | 167 ++++++++++++++++++ client/src/components/Forms/Heroes/index.tsx | 13 +- .../components/Heroes/HeroesCountBadge.tsx | 17 ++ .../src/components/Heroes/HeroesRadarTab.tsx | 95 ++++++++++ .../components/Heroes/HeroesRadarTable.tsx | 123 +++++++++++++ .../components/Profile/PublicFeedbackCard.tsx | 21 +-- .../Profile/PublicFeedbackModal.tsx | 2 +- client/src/configs/heroes-badges.ts | 4 +- client/src/domain/user.ts | 4 + .../Score/components/ScoreTable/index.tsx | 2 +- client/src/pages/heroes.tsx | 19 +- nestjs/src/gratitudes/dto/hero-radar.dto.ts | 37 ++++ .../gratitudes/dto/heroes-radar-badge.dto.ts | 20 +++ .../gratitudes/dto/heroes-radar-query.dto.ts | 34 ++++ nestjs/src/gratitudes/dto/heroes-radar.dto.ts | 30 ++++ nestjs/src/gratitudes/dto/index.ts | 1 + .../src/gratitudes/gratitudes.controller.ts | 14 +- nestjs/src/gratitudes/gratitudes.service.ts | 61 ++++++- nestjs/src/spec.json | 43 +++++ setup/backup-local.sql | 99 +++++++++++ 20 files changed, 775 insertions(+), 31 deletions(-) create mode 100644 client/src/components/Heroes/HeroesCountBadge.tsx create mode 100644 client/src/components/Heroes/HeroesRadarTab.tsx create mode 100644 client/src/components/Heroes/HeroesRadarTable.tsx create mode 100644 nestjs/src/gratitudes/dto/hero-radar.dto.ts create mode 100644 nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts create mode 100644 nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts create mode 100644 nestjs/src/gratitudes/dto/heroes-radar.dto.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index ccdbd6a86c..2c901dcdd5 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -3005,6 +3005,81 @@ export const GratitudeDtoBadgeIdEnum = { export type GratitudeDtoBadgeIdEnum = typeof GratitudeDtoBadgeIdEnum[keyof typeof GratitudeDtoBadgeIdEnum]; +/** + * + * @export + * @interface HeroRadarDto + */ +export interface HeroRadarDto { + /** + * + * @type {string} + * @memberof HeroRadarDto + */ + 'githubId': string; + /** + * + * @type {string} + * @memberof HeroRadarDto + */ + 'name': string; + /** + * + * @type {number} + * @memberof HeroRadarDto + */ + 'rank': number; + /** + * + * @type {number} + * @memberof HeroRadarDto + */ + 'total': number; + /** + * + * @type {Array} + * @memberof HeroRadarDto + */ + 'badges': Array; +} +/** + * + * @export + * @interface HeroesRadarBadgeDto + */ +export interface HeroesRadarBadgeDto { + /** + * + * @type {string} + * @memberof HeroesRadarBadgeDto + */ + 'id': string; + /** + * + * @type {number} + * @memberof HeroesRadarBadgeDto + */ + 'count': number; +} +/** + * + * @export + * @interface HeroesRadarDto + */ +export interface HeroesRadarDto { + /** + * + * @type {Array} + * @memberof HeroesRadarDto + */ + 'content': Array; + /** + * + * @type {PaginationMetaDto} + * @memberof HeroesRadarDto + */ + 'pagination': PaginationMetaDto; +} /** * * @export @@ -11883,6 +11958,59 @@ export const GratitudesApiAxiosParamCreator = function (configuration?: Configur + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} current + * @param {number} pageSize + * @param {number} [courseId] + * @param {boolean} [notActivist] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getHeroesRadar: async (current: number, pageSize: number, courseId?: number, notActivist?: boolean, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'current' is not null or undefined + assertParamExists('getHeroesRadar', 'current', current) + // verify required parameter 'pageSize' is not null or undefined + assertParamExists('getHeroesRadar', 'pageSize', pageSize) + const localVarPath = `/gratitudes/heroes/radar`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (courseId !== undefined) { + localVarQueryParameter['courseId'] = courseId; + } + + if (notActivist !== undefined) { + localVarQueryParameter['notActivist'] = notActivist; + } + + if (current !== undefined) { + localVarQueryParameter['current'] = current; + } + + if (pageSize !== undefined) { + localVarQueryParameter['pageSize'] = pageSize; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -11922,6 +12050,19 @@ export const GratitudesApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getBadges(courseId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} current + * @param {number} pageSize + * @param {number} [courseId] + * @param {boolean} [notActivist] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesRadar(current, pageSize, courseId, notActivist, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -11950,6 +12091,18 @@ export const GratitudesApiFactory = function (configuration?: Configuration, bas getBadges(courseId: number, options?: any): AxiosPromise> { return localVarFp.getBadges(courseId, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} current + * @param {number} pageSize + * @param {number} [courseId] + * @param {boolean} [notActivist] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: any): AxiosPromise { + return localVarFp.getHeroesRadar(current, pageSize, courseId, notActivist, options).then((request) => request(axios, basePath)); + }, }; }; @@ -11981,6 +12134,20 @@ export class GratitudesApi extends BaseAPI { public getBadges(courseId: number, options?: AxiosRequestConfig) { return GratitudesApiFp(this.configuration).getBadges(courseId, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {number} current + * @param {number} pageSize + * @param {number} [courseId] + * @param {boolean} [notActivist] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof GratitudesApi + */ + public getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: AxiosRequestConfig) { + return GratitudesApiFp(this.configuration).getHeroesRadar(current, pageSize, courseId, notActivist, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/client/src/components/Forms/Heroes/index.tsx b/client/src/components/Forms/Heroes/index.tsx index 8d61753853..64baed09b6 100644 --- a/client/src/components/Forms/Heroes/index.tsx +++ b/client/src/components/Forms/Heroes/index.tsx @@ -7,7 +7,8 @@ import { IGratitudeGetRequest, IGratitudeGetResponse, HeroesFormData } from 'com import heroesBadges from 'configs/heroes-badges'; import { GratitudeService } from 'services/gratitude'; import { onlyDefined } from 'utils/onlyDefined'; -import { useActiveCourseContext } from 'modules/Course/contexts'; +import { Course } from 'services/models'; +import { getFullName } from 'domain/user'; const { Text, Link, Paragraph } = Typography; const { useBreakpoint } = Grid; @@ -21,11 +22,7 @@ export const fields = { courseId: 'courseId', } as const; -const getFullName = (user: { firstName: string | null; lastName: string | null; githubId: string }) => - user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : `${user.githubId}`; - -export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void }) => { - const { courses } = useActiveCourseContext(); +export const HeroesForm = ({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) => { const [heroesData, setHeroesData] = useState([]); const [heroesCount, setHeroesCount] = useState(initialPage); const [currentPage, setCurrentPage] = useState(initialPage); @@ -122,7 +119,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
@@ -137,7 +134,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
diff --git a/client/src/components/Heroes/HeroesCountBadge.tsx b/client/src/components/Heroes/HeroesCountBadge.tsx new file mode 100644 index 0000000000..548af7c4ed --- /dev/null +++ b/client/src/components/Heroes/HeroesCountBadge.tsx @@ -0,0 +1,17 @@ +import { Badge, Tooltip, Avatar } from 'antd'; +import { HeroesRadarBadgeDto } from 'api'; +import heroesBadges from 'configs/heroes-badges'; + +function HeroesCountBadge({ badge: { id, count } }: { badge: HeroesRadarBadgeDto }) { + return ( +
+ + + + + +
+ ); +} + +export default HeroesCountBadge; diff --git a/client/src/components/Heroes/HeroesRadarTab.tsx b/client/src/components/Heroes/HeroesRadarTab.tsx new file mode 100644 index 0000000000..c382f82219 --- /dev/null +++ b/client/src/components/Heroes/HeroesRadarTab.tsx @@ -0,0 +1,95 @@ +import { Button, Checkbox, Form, Select, Space, TableProps } from 'antd'; +import HeroesRadarTable from './HeroesRadarTable'; +import { HeroesRadarDto, GratitudesApi, HeroRadarDto } from 'api'; +import { IPaginationInfo } from 'common/types/pagination'; +import { useState, useEffect, useCallback } from 'react'; +import { Course } from 'services/models'; +import { onlyDefined } from 'utils/onlyDefined'; + +export type HeroesRadarFormProps = { + courseId?: number; + notActivist?: boolean; +}; + +type GetHeroesProps = HeroesRadarFormProps & Partial; + +export type LayoutType = Parameters[0]['layout']; + +const initialPage = 1; +const initialPageSize = 20; +const initialQueryParams = { current: initialPage, pageSize: initialPageSize }; + +function HeroesRadarTab({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) { + const [heroes, setHeroes] = useState({ + content: [], + pagination: { current: initialPage, pageSize: initialPageSize, itemCount: 0, total: 0, totalPages: 0 }, + }); + const [form] = Form.useForm(); + const [formData, setFormData] = useState(form.getFieldsValue()); + const [formLayout, setFormLayout] = useState('inline'); + const gratitudeApi = new GratitudesApi(); + + const getHeroes = async ({ + current = initialPage, + pageSize = initialPageSize, + courseId, + notActivist, + }: GetHeroesProps) => { + try { + setLoading(true); + const { data } = await gratitudeApi.getHeroesRadar(current, pageSize, courseId, notActivist); + setHeroes(data); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + getHeroes(initialQueryParams); + }, []); + + const handleSubmit = useCallback(async (formData: HeroesRadarFormProps) => { + const data = onlyDefined(formData); + setFormData(data); + await getHeroes(data); + }, []); + + const onClear = useCallback(async () => { + form.resetFields(); + setFormData(form.getFieldsValue()); + await getHeroes(initialQueryParams); + }, []); + + const handleChange: TableProps['onChange'] = async ({ current, pageSize }) => { + try { + setLoading(true); + await getHeroes({ current, pageSize, ...formData }); + } finally { + setLoading(false); + } + }; + + return ( + <> +
+ +