From 08cc19309d8d9cd4906e0f1bbd9727c8f3cbbdf5 Mon Sep 17 00:00:00 2001 From: Piotr Gawron <p.gawron@atcomp.pl> Date: Wed, 12 Jun 2024 15:49:03 +0200 Subject: [PATCH] fetch projects --- .../AccessDeniedModal.component.tsx | 7 ++++++- src/models/fixtures/projectsFixture.ts | 4 ++-- src/models/pageableSchema.ts | 12 ++++++++++++ src/redux/middlewares/error.middleware.ts | 2 ++ src/redux/projects/projects.constants.ts | 1 + src/redux/projects/projects.reducers.test.ts | 14 ++++++++------ src/redux/projects/projects.selectors.ts | 5 +++++ src/redux/projects/projects.thunks.ts | 14 ++++++++------ src/types/models.ts | 9 +++++++++ src/utils/validateDataUsingZodSchema.ts | 2 ++ 10 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 src/models/pageableSchema.ts create mode 100644 src/redux/projects/projects.constants.ts create mode 100644 src/redux/projects/projects.selectors.ts diff --git a/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx b/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx index 970ca5d6..2b821edf 100644 --- a/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx +++ b/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx @@ -3,18 +3,22 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { loginUserSelector } from '@/redux/user/user.selectors'; import { openLoginModal } from '@/redux/modal/modal.slice'; -import { MINUS_ONE } from '@/constants/common'; +import { MINUS_ONE, ZERO } from '@/constants/common'; import { Button } from '@/shared/Button'; import { adminEmailValSelector } from '@/redux/configuration/configuration.selectors'; +import { projectsSelector } from '@/redux/projects/projects.selectors'; export const AccessDeniedModal: React.FC = () => { const dispatch = useAppDispatch(); const login = useAppSelector(loginUserSelector); + const projects = useAppSelector(projectsSelector); const adminEmail = useAppSelector(adminEmailValSelector); const isAnonymousLogin = !login; + const isProjectsAvailable = projects.length > ZERO; + const isAdminEmail = adminEmail !== '' && adminEmail !== undefined; const handleGoBack = async (e: React.FormEvent<HTMLButtonElement>): Promise<void> => { @@ -52,6 +56,7 @@ export const AccessDeniedModal: React.FC = () => { </div> </div> )} + {isProjectsAvailable && <div>Switch to another map</div>} {isAdminEmail && ( <div className="mt-1 text-center"> <Button diff --git a/src/models/fixtures/projectsFixture.ts b/src/models/fixtures/projectsFixture.ts index b845ca59..c625481f 100644 --- a/src/models/fixtures/projectsFixture.ts +++ b/src/models/fixtures/projectsFixture.ts @@ -1,10 +1,10 @@ import { ZOD_SEED } from '@/constants'; // eslint-disable-next-line import/no-extraneous-dependencies import { createFixture } from 'zod-fixture'; -import { z } from 'zod'; +import { pageableSchema } from '@/models/pageableSchema'; import { projectSchema } from '../projectSchema'; -export const projectsFixture = createFixture(z.array(projectSchema), { +export const projectPageFixture = createFixture(pageableSchema(projectSchema), { seed: ZOD_SEED, array: { min: 1, max: 1 }, }); diff --git a/src/models/pageableSchema.ts b/src/models/pageableSchema.ts new file mode 100644 index 00000000..94c544ee --- /dev/null +++ b/src/models/pageableSchema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; +import { ZodTypeAny } from 'zod/lib/types'; + +export const pageableSchema = <T extends ZodTypeAny>(type: T): ZodTypeAny => + z.object({ + totalPages: z.number().nonnegative(), + totalElements: z.number().nonnegative(), + numberOfElements: z.number().nonnegative(), + size: z.number().positive(), + number: z.number().nonnegative(), + content: z.array(type), + }); diff --git a/src/redux/middlewares/error.middleware.ts b/src/redux/middlewares/error.middleware.ts index 0dd789dc..56ba18c0 100644 --- a/src/redux/middlewares/error.middleware.ts +++ b/src/redux/middlewares/error.middleware.ts @@ -2,6 +2,7 @@ import type { AppListenerEffectAPI, AppStartListening } from '@/redux/store'; import { Action, createListenerMiddleware, isRejected } from '@reduxjs/toolkit'; import { createErrorData } from '@/utils/error-report/errorReporting'; import { openAccessDeniedModal, openErrorReportModal } from '@/redux/modal/modal.slice'; +import { getProjects } from '@/redux/projects/projects.thunks'; export const errorListenerMiddleware = createListenerMiddleware(); @@ -13,6 +14,7 @@ export const errorMiddlewareListener = async ( ): Promise<void> => { if (isRejected(action) && action.type !== 'user/getSessionValid/rejected') { if (action.error.code === '403') { + dispatch(getProjects()); dispatch(openAccessDeniedModal()); } else { const errorData = await createErrorData(action.error, getState()); diff --git a/src/redux/projects/projects.constants.ts b/src/redux/projects/projects.constants.ts new file mode 100644 index 00000000..a7fb0f8b --- /dev/null +++ b/src/redux/projects/projects.constants.ts @@ -0,0 +1 @@ +export const PROJECTS_FETCHING_ERROR_PREFIX = 'Failed to fetch projects'; diff --git a/src/redux/projects/projects.reducers.test.ts b/src/redux/projects/projects.reducers.test.ts index df5a0041..2912689e 100644 --- a/src/redux/projects/projects.reducers.test.ts +++ b/src/redux/projects/projects.reducers.test.ts @@ -1,4 +1,4 @@ -import { projectsFixture } from '@/models/fixtures/projectsFixture'; +import { projectPageFixture } from '@/models/fixtures/projectsFixture'; import { ToolkitStoreWithSingleSlice, createStoreInstanceUsingSliceReducer, @@ -31,7 +31,7 @@ describe('projects reducer', () => { expect(projectsReducer(undefined, action)).toEqual(INITIAL_STATE); }); it('should update store after successfull getProjects query', async () => { - mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectsFixture); + mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectPageFixture); const { type } = await store.dispatch(getProjects()); const { data, loading, error } = store.getState().projects; @@ -39,7 +39,7 @@ describe('projects reducer', () => { expect(type).toBe('project/getProjects/fulfilled'); expect(loading).toEqual('succeeded'); expect(error).toEqual({ message: '', name: '' }); - expect(data).toEqual(projectsFixture); + expect(data).toEqual(projectPageFixture.content); }); it('should update store after failed getProjects query', async () => { @@ -49,14 +49,16 @@ describe('projects reducer', () => { const { data, loading, error } = store.getState().projects; expect(action.type).toBe('project/getProjects/rejected'); - expect(() => unwrapResult(action)).toThrow('Request failed with status code 404'); + expect(() => unwrapResult(action)).toThrow( + "Failed to fetch projects: The page you're looking for doesn't exist. Please verify the URL and try again.", + ); expect(loading).toEqual('failed'); expect(error).toEqual({ message: '', name: '' }); expect(data).toEqual([]); }); it('should update store on loading getProjects query', async () => { - mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectsFixture); + mockedAxiosClient.onGet(apiPath.getProjects()).reply(HttpStatusCode.Ok, projectPageFixture); const actionPromise = store.dispatch(getProjects()); @@ -67,7 +69,7 @@ describe('projects reducer', () => { actionPromise.then(() => { const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().projects; - expect(dataPromiseFulfilled).toEqual(projectsFixture); + expect(dataPromiseFulfilled).toEqual(projectPageFixture.content); expect(promiseFulfilled).toEqual('succeeded'); }); }); diff --git a/src/redux/projects/projects.selectors.ts b/src/redux/projects/projects.selectors.ts new file mode 100644 index 00000000..fb4ed5c4 --- /dev/null +++ b/src/redux/projects/projects.selectors.ts @@ -0,0 +1,5 @@ +import { rootSelector } from '@/redux/root/root.selectors'; +import { createSelector } from '@reduxjs/toolkit'; + +export const projectsRootSelector = createSelector(rootSelector, state => state.projects); +export const projectsSelector = createSelector(projectsRootSelector, state => state.data); diff --git a/src/redux/projects/projects.thunks.ts b/src/redux/projects/projects.thunks.ts index c0853db1..b3715ea7 100644 --- a/src/redux/projects/projects.thunks.ts +++ b/src/redux/projects/projects.thunks.ts @@ -1,23 +1,25 @@ import { axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; import { projectSchema } from '@/models/projectSchema'; -import { Project } from '@/types/models'; +import { PageOf, Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ThunkConfig } from '@/types/store'; -import { z } from 'zod'; +import { getError } from '@/utils/error-report/getError'; +import { PROJECTS_FETCHING_ERROR_PREFIX } from '@/redux/projects/projects.constants'; +import { pageableSchema } from '@/models/pageableSchema'; import { apiPath } from '../apiPath'; export const getProjects = createAsyncThunk<Project[], void, ThunkConfig>( 'project/getProjects', async () => { try { - const response = await axiosInstanceNewAPI.get<Project[]>(apiPath.getProjects()); + const response = await axiosInstanceNewAPI.get<PageOf<Project>>(apiPath.getProjects()); - const isDataValid = validateDataUsingZodSchema(response.data, z.array(projectSchema)); + const isDataValid = validateDataUsingZodSchema(response.data, pageableSchema(projectSchema)); - return isDataValid ? response.data : []; + return isDataValid ? response.data.content : []; } catch (error) { - return Promise.reject(error); + return Promise.reject(getError({ error, prefix: PROJECTS_FETCHING_ERROR_PREFIX })); } }, ); diff --git a/src/types/models.ts b/src/types/models.ts index 57a80ea1..3cdb0f81 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -122,3 +122,12 @@ export type MarkerLine = z.infer<typeof markerLineSchema>; export type MarkerWithPosition = z.infer<typeof markerWithPositionSchema>; export type Marker = z.infer<typeof markerSchema>; export type JavaStacktrace = z.infer<typeof javaStacktraceSchema>; + +export type PageOf<T> = { + totalPages: number; + totalElements: number; + numberOfElements: number; + size: number; + number: number; + content: T[]; +}; diff --git a/src/utils/validateDataUsingZodSchema.ts b/src/utils/validateDataUsingZodSchema.ts index a7b0cf08..704b4bac 100644 --- a/src/utils/validateDataUsingZodSchema.ts +++ b/src/utils/validateDataUsingZodSchema.ts @@ -9,6 +9,8 @@ export const validateDataUsingZodSchema: IsApiResponseValid = (data, schema: Zod // TODO - probably need to rething way of handling parsing errors, for now let's leave it to console.log // eslint-disable-next-line no-console console.error('Error on parsing data', validationResults.error); + // eslint-disable-next-line no-console + console.error(validationResults.error.message); } return validationResults.success; -- GitLab