diff --git a/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx b/src/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component.tsx index 970ca5d6aadb815a9ab5ed2e29cc948508fcaaf6..2b821edfe83fb70955b0d7094c0864bd04380424 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 b845ca59e6a2ab51ba6bc547a3c5baa6f040fd3e..c625481f22a4209cead2372ea09a0d98aa94c85a 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 0000000000000000000000000000000000000000..94c544ee3909ae23cd73b48f3ddb4e9da1383820 --- /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 0dd789dc975780854f161336e7e901ee08b7ee65..56ba18c0f5cfcef3435ac3d6b86aa2e8db452f45 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 0000000000000000000000000000000000000000..a7fb0f8bc731b88c03942145baf6c4f4413b8df4 --- /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 df5a0041e146d81d6312d999f95f0d1f80eb0bb6..2912689e2e33d2fd4b8c30e2ad2b858e2def6a28 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 0000000000000000000000000000000000000000..fb4ed5c4ec056e07bf3d242f96da94549def1f44 --- /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 c0853db1ead234138200ba753b9b35fe5cfe7cd2..b3715ea773daa96201d55d43f6c4be53fd9da86c 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 57a80ea1f2197c7218258246eeca684f677cf702..3cdb0f81e60bdc90f02833ce92deeb72edd7b399 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 a7b0cf0858cc2e62842047cfea56bb5066324457..704b4bacd23d801e974dd59c55d3378c439f322a 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;