refactor: update propTypes -> TypeScript in Studio Header (#643)
This commit is contained in:
@@ -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<Props> = ({
|
||||
studioBaseUrl,
|
||||
logo,
|
||||
logoAltText,
|
||||
@@ -16,10 +21,4 @@ const BrandNav = ({
|
||||
</Link>
|
||||
);
|
||||
|
||||
BrandNav.propTypes = {
|
||||
studioBaseUrl: PropTypes.string.isRequired,
|
||||
logo: PropTypes.string.isRequired,
|
||||
logoAltText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BrandNav;
|
||||
|
||||
@@ -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<Props> = ({
|
||||
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;
|
||||
|
||||
@@ -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<HTMLButtonElement>;
|
||||
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<HTMLButtonElement>;
|
||||
containerProps?: Omit<ComponentProps<typeof Container>, '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 (
|
||||
<Container
|
||||
@@ -144,53 +169,4 @@ const HeaderBody = ({
|
||||
);
|
||||
};
|
||||
|
||||
HeaderBody.propTypes = {
|
||||
studioBaseUrl: PropTypes.string.isRequired,
|
||||
logoutUrl: PropTypes.string.isRequired,
|
||||
setModalPopupTarget: PropTypes.func,
|
||||
toggleModalPopup: PropTypes.func,
|
||||
isModalPopupOpen: PropTypes.bool,
|
||||
number: PropTypes.string,
|
||||
org: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logo: PropTypes.string,
|
||||
logoAltText: PropTypes.string,
|
||||
authenticatedUserAvatar: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
isMobile: PropTypes.bool,
|
||||
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,
|
||||
containerProps: PropTypes.shape(Container.propTypes),
|
||||
};
|
||||
|
||||
HeaderBody.defaultProps = {
|
||||
setModalPopupTarget: null,
|
||||
toggleModalPopup: null,
|
||||
isModalPopupOpen: false,
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
number: '',
|
||||
org: '',
|
||||
title: '',
|
||||
authenticatedUserAvatar: null,
|
||||
username: null,
|
||||
isAdmin: false,
|
||||
isMobile: false,
|
||||
isHiddenMainMenu: false,
|
||||
mainMenuDropdowns: [],
|
||||
outlineLink: null,
|
||||
searchButtonAction: null,
|
||||
containerProps: {},
|
||||
};
|
||||
|
||||
export default HeaderBody;
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { type FunctionComponent, useState } from 'react';
|
||||
import { useToggle, ModalPopup } from '@openedx/paragon';
|
||||
import HeaderBody from './HeaderBody';
|
||||
import HeaderBody, { type HeaderBodyProps } from './HeaderBody';
|
||||
import MobileMenu from './MobileMenu';
|
||||
|
||||
const MobileHeader = ({
|
||||
type Props = Pick<HeaderBodyProps,
|
||||
| 'studioBaseUrl'
|
||||
| 'logoutUrl'
|
||||
| 'number'
|
||||
| 'org'
|
||||
| 'title'
|
||||
| 'logo'
|
||||
| 'logoAltText'
|
||||
| 'authenticatedUserAvatar'
|
||||
| 'username'
|
||||
| 'isAdmin'
|
||||
| 'mainMenuDropdowns'
|
||||
| 'outlineLink'
|
||||
>;
|
||||
|
||||
const MobileHeader: FunctionComponent<Props> = ({
|
||||
mainMenuDropdowns,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, , close, toggle] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const [target, setTarget] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */}
|
||||
<HeaderBody
|
||||
{...props}
|
||||
isMobile
|
||||
@@ -36,39 +49,4 @@ const MobileHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
MobileHeader.propTypes = {
|
||||
studioBaseUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
logoutUrl: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
number: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
org: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
title: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
logo: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
logoAltText: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
authenticatedUserAvatar: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
username: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
isAdmin: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
|
||||
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, // eslint-disable-line react/no-unused-prop-types
|
||||
};
|
||||
|
||||
MobileHeader.defaultProps = {
|
||||
logo: null,
|
||||
logoAltText: null,
|
||||
number: null,
|
||||
org: null,
|
||||
title: null,
|
||||
authenticatedUserAvatar: null,
|
||||
username: null,
|
||||
isAdmin: false,
|
||||
mainMenuDropdowns: [],
|
||||
outlineLink: null,
|
||||
};
|
||||
|
||||
export default MobileHeader;
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
buttonTitle: ReactNode;
|
||||
items: { title: ReactNode; href: string; }[];
|
||||
}
|
||||
|
||||
const NavDropdownMenu = ({
|
||||
id,
|
||||
buttonTitle,
|
||||
items,
|
||||
}) => (
|
||||
}: Props) => (
|
||||
<DropdownButton
|
||||
id={id}
|
||||
title={buttonTitle}
|
||||
@@ -30,13 +35,4 @@ const NavDropdownMenu = ({
|
||||
</DropdownButton>
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
@@ -71,12 +71,8 @@ const props: React.ComponentProps<typeof StudioHeader> = {
|
||||
},
|
||||
],
|
||||
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(<RootWrapper {...testProps} />);
|
||||
expect(queryByRole('button', { name: 'Search content' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -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<HeaderBodyProps,
|
||||
| 'number'
|
||||
| 'org'
|
||||
| 'title'
|
||||
| 'containerProps'
|
||||
| 'isHiddenMainMenu'
|
||||
| 'mainMenuDropdowns'
|
||||
| 'outlineLink'
|
||||
| 'searchButtonAction'
|
||||
> & {
|
||||
isNewHomePage: boolean;
|
||||
};
|
||||
|
||||
const StudioHeader: FunctionComponent<Props> = ({
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user