From 24ed4bcef134ba42115f4ec41be876a0b482a722 Mon Sep 17 00:00:00 2001 From: Hamza Israr Date: Tue, 19 May 2026 11:12:23 +0500 Subject: [PATCH 1/2] fix: Sync pagination state after course data fetch Update currentPage in Redux after fetching courses in the thunk so the pagination UI stays in sync with the fetched page content on navigation. Fixes: Edly-8271 --- src/studio-home/data/thunks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js index 12afbfc1b6..c8011c067c 100644 --- a/src/studio-home/data/thunks.js +++ b/src/studio-home/data/thunks.js @@ -10,6 +10,7 @@ import { updateLoadingStatuses, updateSavingStatuses, fetchCourseDataSuccessV2, + updateStudioHomeCoursesCustomParams, } from './slice'; /** @@ -43,6 +44,7 @@ function fetchStudioHomeData( try { const coursesData = await getStudioHomeCoursesV2(search || '', requestParams); dispatch(fetchCourseDataSuccessV2(coursesData)); + dispatch(updateStudioHomeCoursesCustomParams({ currentPage: requestParams.page || 1 })); dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL })); } catch { dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED })); From d4cccee4c039e9274580444c308500965048ef01 Mon Sep 17 00:00:00 2001 From: Hamza Israr Date: Mon, 25 May 2026 07:30:44 +0500 Subject: [PATCH 2/2] fix: Derive page from both requestParams and search query string Parse page from the search query string as a fallback when requestParams.page is not set, to handle deep links like /home?page=2 correctly. Add regression tests. Fixes: Edly-8271 --- src/studio-home/data/thunks.js | 4 +- src/studio-home/data/thunks.test.js | 78 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/studio-home/data/thunks.test.js diff --git a/src/studio-home/data/thunks.js b/src/studio-home/data/thunks.js index c8011c067c..32b0a018e8 100644 --- a/src/studio-home/data/thunks.js +++ b/src/studio-home/data/thunks.js @@ -44,7 +44,9 @@ function fetchStudioHomeData( try { const coursesData = await getStudioHomeCoursesV2(search || '', requestParams); dispatch(fetchCourseDataSuccessV2(coursesData)); - dispatch(updateStudioHomeCoursesCustomParams({ currentPage: requestParams.page || 1 })); + const searchPage = search ? parseInt(new URLSearchParams(search).get('page'), 10) : NaN; + const fetchedPage = requestParams.page || (!Number.isNaN(searchPage) && searchPage > 0 ? searchPage : 1); + dispatch(updateStudioHomeCoursesCustomParams({ currentPage: fetchedPage })); dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL })); } catch { dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED })); diff --git a/src/studio-home/data/thunks.test.js b/src/studio-home/data/thunks.test.js new file mode 100644 index 0000000000..2030be2d8b --- /dev/null +++ b/src/studio-home/data/thunks.test.js @@ -0,0 +1,78 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { fetchStudioHomeData } from './thunks'; +import { getApiBaseUrl, getStudioHomeApiUrl } from './api'; +import { + generateGetStudioCoursesApiResponseV2, + generateGetStudioHomeDataApiResponse, +} from '../factories/mockApiResponses'; + +let axiosMock; +let dispatch; + +describe('fetchStudioHomeData thunk', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + dispatch = jest.fn(); + + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse()); + axiosMock.onGet(new RegExp(`${getApiBaseUrl()}/api/contentstore/v2/home/courses.*`)) + .reply(200, generateGetStudioCoursesApiResponseV2()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should sync currentPage from requestParams.page after fetching courses', async () => { + const requestParams = { page: 3 }; + await fetchStudioHomeData('', false, requestParams)(dispatch); + + const updateParamsCall = dispatch.mock.calls.find( + ([action]) => action.type === 'studioHome/updateStudioHomeCoursesCustomParams', + ); + expect(updateParamsCall).toBeDefined(); + expect(updateParamsCall[0].payload).toEqual({ currentPage: 3 }); + }); + + it('should sync currentPage from search query string when page is not in requestParams', async () => { + await fetchStudioHomeData('?page=2', false, {})(dispatch); + + const updateParamsCall = dispatch.mock.calls.find( + ([action]) => action.type === 'studioHome/updateStudioHomeCoursesCustomParams', + ); + expect(updateParamsCall).toBeDefined(); + expect(updateParamsCall[0].payload).toEqual({ currentPage: 2 }); + }); + + it('should default currentPage to 1 when page is not specified anywhere', async () => { + await fetchStudioHomeData('', false, {})(dispatch); + + const updateParamsCall = dispatch.mock.calls.find( + ([action]) => action.type === 'studioHome/updateStudioHomeCoursesCustomParams', + ); + expect(updateParamsCall).toBeDefined(); + expect(updateParamsCall[0].payload).toEqual({ currentPage: 1 }); + }); + + it('should prefer requestParams.page over search query string page', async () => { + const requestParams = { page: 5 }; + await fetchStudioHomeData('?page=2', false, requestParams)(dispatch); + + const updateParamsCall = dispatch.mock.calls.find( + ([action]) => action.type === 'studioHome/updateStudioHomeCoursesCustomParams', + ); + expect(updateParamsCall).toBeDefined(); + expect(updateParamsCall[0].payload).toEqual({ currentPage: 5 }); + }); +});