From 970b278033d2eaa0553e25e68dcd42118ca89d0a Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 7 Aug 2026 17:20:00 -0400 Subject: [PATCH] refactor: convert TabPage to TypeScript Behavior-preserving conversion of TabPage.jsx -> TabPage.tsx. - Typed props (courseId/unitId optional, courseStatus: StatusValue) and useSelector state via the store's RootState. - Render LoadedTabPage only when courseId is present, narrowing its required courseId prop without a cast. Co-Authored-By: Claude Opus 4.8 --- src/tab-page/TabPage.test.jsx | 1 + src/tab-page/{TabPage.jsx => TabPage.tsx} | 71 +++++++++++++---------- 2 files changed, 40 insertions(+), 32 deletions(-) rename src/tab-page/{TabPage.jsx => TabPage.tsx} (65%) diff --git a/src/tab-page/TabPage.test.jsx b/src/tab-page/TabPage.test.jsx index 8e7e9650c3..33b50f8beb 100644 --- a/src/tab-page/TabPage.test.jsx +++ b/src/tab-page/TabPage.test.jsx @@ -24,6 +24,7 @@ const mockUseToast = (overrides = {}) => useToast.mockReturnValue({ describe('Tab Page', () => { const mockData = { + courseId: 'test-course', courseStatus: 'loaded', }; diff --git a/src/tab-page/TabPage.jsx b/src/tab-page/TabPage.tsx similarity index 65% rename from src/tab-page/TabPage.jsx rename to src/tab-page/TabPage.tsx index 861713741f..ba16d4bfd0 100644 --- a/src/tab-page/TabPage.jsx +++ b/src/tab-page/TabPage.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { type ReactNode } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useSelector } from 'react-redux'; import { Navigate } from 'react-router-dom'; @@ -11,6 +10,10 @@ import PageLoading from '../generic/PageLoading'; import { getAccessDeniedRedirectUrl } from '../shared/access'; import { useModel } from '../generic/model-store'; import { useToast } from '../generic/ToastContext'; +import type { RootState } from '../store'; +import { + LOADING, LOADED, DENIED, type StatusValue, +} from '../constants'; import genericMessages from '../generic/messages'; import messages from './messages'; @@ -18,20 +21,30 @@ import LoadedTabPage from './LoadedTabPage'; import LaunchCourseHomeTourButton from '../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton'; import { TourProvider } from '../product-tours/TourContext'; -const TabPage = (props) => { +interface TabPageProps { + activeTabSlug: string; + courseId?: string; + courseStatus: StatusValue; + metadataModel: string; + unitId?: string; + children?: ReactNode; +} + +const TabPage = ({ + activeTabSlug, + courseId, + courseStatus, + metadataModel, + unitId, + children, +}: TabPageProps) => { const intl = useIntl(); - const { - activeTabSlug, - courseId, - courseStatus, - metadataModel, - } = props; const { errorMessage: courseHomeErrorMessage, - } = useSelector(state => state.courseHome); + } = useSelector((state: RootState) => state.courseHome); const { errorMessage: coursewareErrorMessage, - } = useSelector(state => state.courseware); + } = useSelector((state: RootState) => state.courseware); const errorMessage = courseHomeErrorMessage || coursewareErrorMessage; const { toastContent, isToastOpen, closeToast } = useToast(); const { @@ -42,7 +55,7 @@ const TabPage = (props) => { title, } = useModel('courseHomeMeta', courseId); - if (courseStatus === 'denied') { + if (courseStatus === DENIED) { const redirectUrl = getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start); if (redirectUrl) { return (); @@ -51,15 +64,15 @@ const TabPage = (props) => { return ( - {['loaded', 'denied'].includes(courseStatus) && ( + {(courseStatus === LOADED || courseStatus === DENIED) && ( <> - {toastContent?.message} + {toastContent?.message ?? ''} {metadataModel === 'courseHomeMeta' && ()} @@ -67,16 +80,23 @@ const TabPage = (props) => { - {courseStatus === 'loading' && ( + {courseStatus === LOADING && ( )} - {['loaded', 'denied'].includes(courseStatus) && ( - + {(courseStatus === LOADED || courseStatus === DENIED) && courseId && ( + + {children} + )} {/* courseStatus 'failed' and any other unexpected course status. */} - {(!['loading', 'loaded', 'denied'].includes(courseStatus)) && ( + {courseStatus !== LOADING && courseStatus !== LOADED && courseStatus !== DENIED && (

{errorMessage || intl.formatMessage(messages.failure)}

@@ -86,17 +106,4 @@ const TabPage = (props) => { ); }; -TabPage.defaultProps = { - courseId: null, - unitId: null, -}; - -TabPage.propTypes = { - activeTabSlug: PropTypes.string.isRequired, - courseId: PropTypes.string, - courseStatus: PropTypes.string.isRequired, - metadataModel: PropTypes.string.isRequired, - unitId: PropTypes.string, -}; - export default TabPage;