From 413454b6e61d6e3c237a3ff212b3d47deae1dd5a Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 14 Oct 2025 09:43:33 -0700 Subject: [PATCH] refactor: update propTypes -> TypeScript in Studio Header (#643) --- src/studio-header/BrandNav.tsx | 17 +++-- src/studio-header/CourseLockUp.tsx | 38 ++++------ src/studio-header/HeaderBody.tsx | 92 +++++++++---------------- src/studio-header/MobileHeader.tsx | 60 +++++----------- src/studio-header/NavDropdownMenu.tsx | 20 +++--- src/studio-header/StudioHeader.test.tsx | 8 +-- src/studio-header/StudioHeader.tsx | 60 +++++++--------- 7 files changed, 110 insertions(+), 185 deletions(-) diff --git a/src/studio-header/BrandNav.tsx b/src/studio-header/BrandNav.tsx index fd9ecd0..5e3bba3 100644 --- a/src/studio-header/BrandNav.tsx +++ b/src/studio-header/BrandNav.tsx @@ -1,8 +1,13 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { type FunctionComponent } from 'react'; import { Link } from 'react-router-dom'; -const BrandNav = ({ +interface Props { + studioBaseUrl: string; + logo: string; + logoAltText: string; +} + +const BrandNav: FunctionComponent = ({ studioBaseUrl, logo, logoAltText, @@ -16,10 +21,4 @@ const BrandNav = ({ ); -BrandNav.propTypes = { - studioBaseUrl: PropTypes.string.isRequired, - logo: PropTypes.string.isRequired, - logoAltText: PropTypes.string.isRequired, -}; - export default BrandNav; diff --git a/src/studio-header/CourseLockUp.tsx b/src/studio-header/CourseLockUp.tsx index d0c687a..8539d16 100644 --- a/src/studio-header/CourseLockUp.tsx +++ b/src/studio-header/CourseLockUp.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { type FunctionComponent } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { OverlayTrigger, @@ -9,14 +8,19 @@ import { Link } from 'react-router-dom'; import messages from './messages'; -const CourseLockUp = ( - { - outlineLink, - org, - number, - title, - }, -) => { +interface Props { + outlineLink?: string; + org?: string; + number?: string; + title?: string; +} + +const CourseLockUp: FunctionComponent = ({ + outlineLink = '', + org = '', + number = '', + title = '', +}) => { const intl = useIntl(); return ( @@ -41,18 +45,4 @@ const CourseLockUp = ( ); }; -CourseLockUp.propTypes = { - number: PropTypes.string, - org: PropTypes.string, - title: PropTypes.string, - outlineLink: PropTypes.string, -}; - -CourseLockUp.defaultProps = { - number: null, - org: null, - title: null, - outlineLink: null, -}; - export default CourseLockUp; diff --git a/src/studio-header/HeaderBody.tsx b/src/studio-header/HeaderBody.tsx index 598aec3..d276159 100644 --- a/src/studio-header/HeaderBody.tsx +++ b/src/studio-header/HeaderBody.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { type ReactNode, type ComponentProps } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; import { @@ -19,6 +18,32 @@ import BrandNav from './BrandNav'; import NavDropdownMenu from './NavDropdownMenu'; import messages from './messages'; +export interface HeaderBodyProps { + studioBaseUrl: string; + logoutUrl: string; + setModalPopupTarget?: ((instance: HTMLButtonElement | null) => void) | null; + toggleModalPopup?: React.MouseEventHandler; + isModalPopupOpen?: boolean; + number?: string; + org?: string; + title: string; + logo: string; + logoAltText: string; + authenticatedUserAvatar?: string; + username?: string; + isAdmin?: boolean; + isMobile?: boolean; + isHiddenMainMenu?: boolean; + mainMenuDropdowns?: { + id: string; + buttonTitle: ReactNode; + items: { title: ReactNode; href: string; }[]; + }[]; + outlineLink?: string; + searchButtonAction?: React.MouseEventHandler; + containerProps?: Omit, 'children'>; +} + const HeaderBody = ({ logo, logoAltText, @@ -31,15 +56,15 @@ const HeaderBody = ({ logoutUrl, authenticatedUserAvatar, isMobile, - setModalPopupTarget, + setModalPopupTarget = null, toggleModalPopup, - isModalPopupOpen, - isHiddenMainMenu, - mainMenuDropdowns, + isModalPopupOpen = false, + isHiddenMainMenu = false, + mainMenuDropdowns = [], outlineLink, searchButtonAction, - containerProps, -}) => { + containerProps = {}, +}: HeaderBodyProps) => { const intl = useIntl(); const renderBrandNav = ( @@ -52,7 +77,7 @@ const HeaderBody = ({ /> ); - const { className: containerClassName, ...restContainerProps } = containerProps || {}; + const { className: containerClassName, ...restContainerProps } = containerProps; return ( ; + +const MobileHeader: FunctionComponent = ({ mainMenuDropdowns, ...props }) => { const [isOpen, , close, toggle] = useToggle(false); - const [target, setTarget] = useState(null); + const [target, setTarget] = useState(null); return ( <> - {/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */} ( +}: Props) => ( ); -NavDropdownMenu.propTypes = { - id: PropTypes.string.isRequired, - buttonTitle: PropTypes.node.isRequired, - items: PropTypes.arrayOf(PropTypes.shape({ - href: PropTypes.string.isRequired, - title: PropTypes.node.isRequired, - })).isRequired, -}; - export default NavDropdownMenu; diff --git a/src/studio-header/StudioHeader.test.tsx b/src/studio-header/StudioHeader.test.tsx index 9e8cd68..0ff402a 100644 --- a/src/studio-header/StudioHeader.test.tsx +++ b/src/studio-header/StudioHeader.test.tsx @@ -71,12 +71,8 @@ const props: React.ComponentProps = { }, ], outlineLink: 'tEsTLInK', - searchButtonAction: null, + searchButtonAction: undefined, isNewHomePage: true, - // These default values shouldn't be needed but typescript is confused by propTypes; can remove after converting - // from propTypes to TypeScript: - containerProps: {}, - isHiddenMainMenu: false, }; describe('Header', () => { @@ -146,7 +142,7 @@ describe('Header', () => { }); it('should not show search button', async () => { - const testProps = { ...props, searchButtonAction: null }; + const testProps = { ...props, searchButtonAction: undefined }; const { queryByRole } = render(); expect(queryByRole('button', { name: 'Search content' })).not.toBeInTheDocument(); }); diff --git a/src/studio-header/StudioHeader.tsx b/src/studio-header/StudioHeader.tsx index 735cfa5..9d20bed 100644 --- a/src/studio-header/StudioHeader.tsx +++ b/src/studio-header/StudioHeader.tsx @@ -1,11 +1,10 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; +import React, { type FunctionComponent, useContext } from 'react'; import Responsive from 'react-responsive'; import { AppContext } from '@edx/frontend-platform/react'; import { ensureConfig } from '@edx/frontend-platform'; import MobileHeader from './MobileHeader'; -import HeaderBody from './HeaderBody'; +import HeaderBody, { HeaderBodyProps } from './HeaderBody'; ensureConfig([ 'STUDIO_BASE_URL', @@ -15,9 +14,29 @@ ensureConfig([ 'LOGO_URL', ], 'Studio Header component'); -const StudioHeader = ({ - number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns, - outlineLink, searchButtonAction, isNewHomePage, +type Props = Pick & { + isNewHomePage: boolean; +}; + +const StudioHeader: FunctionComponent = ({ + number, + org, + title, + containerProps, + isHiddenMainMenu, + mainMenuDropdowns, + outlineLink, + searchButtonAction, + isNewHomePage, }) => { // @ts-expect-error - frontend-platform doesn't yet have type information :/ const { authenticatedUser, config } = useContext(AppContext); @@ -52,33 +71,4 @@ const StudioHeader = ({ ); }; -StudioHeader.propTypes = { - number: PropTypes.string, - org: PropTypes.string, - title: PropTypes.string.isRequired, - containerProps: HeaderBody.propTypes.containerProps, - isHiddenMainMenu: PropTypes.bool, - mainMenuDropdowns: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - buttonTitle: PropTypes.node, - items: PropTypes.arrayOf(PropTypes.shape({ - href: PropTypes.string, - title: PropTypes.node, - })), - })), - outlineLink: PropTypes.string, - searchButtonAction: PropTypes.func, - isNewHomePage: PropTypes.bool.isRequired, -}; - -StudioHeader.defaultProps = { - number: '', - org: '', - containerProps: {}, - isHiddenMainMenu: false, - mainMenuDropdowns: [], - outlineLink: null, - searchButtonAction: null, -}; - export default StudioHeader;