diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.js b/packages/core/src/layout/ContentHeader/ContentHeader.js deleted file mode 100644 index dac44354a4..0000000000 --- a/packages/core/src/layout/ContentHeader/ContentHeader.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { Component, Fragment } from 'react'; -import { Typography, withStyles } from '@material-ui/core'; -import Helmet from 'react-helmet'; -// import FavoriteButton from 'shared/components/layout/FavoriteButton'; - -const styles = theme => ({ - container: { - width: '100%', - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'flex-end', - alignItems: 'center', - }, - leftItemsBox: { - flex: '1 1 auto', - marginBottom: theme.spacing(1), - minWidth: 0, - overflow: 'visible', - }, - rightItemsBox: { - flex: '0 1 auto', - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - alignItems: 'center', - marginLeft: theme.spacing(1), - marginBottom: theme.spacing(1), - minWidth: 0, - overflow: 'visible', - }, - description: {}, - title: { - display: 'inline-flex', - }, -}); - -class ContentHeader extends Component { - static defaultProps = { - favoriteable: true, - title: 'Unknown page', - titleComponent: undefined, - }; - - render() { - const { - title, - description, - /* favoriteable,*/ children, - classes, - } = this.props; - const TitleComponent = this.props.titleComponent; - const renderedTitle = - TitleComponent !== undefined ? ( - - ) : ( - - {title} - - ); - - return ( - - -
-
- {renderedTitle} - {/* favoriteable && */} - {description && ( - - {description} - - )} -
-
{children}
-
-
- ); - } -} - -export default withStyles(styles)(ContentHeader); diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx new file mode 100644 index 0000000000..50b4f921ac --- /dev/null +++ b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import ContentHeader from './ContentHeader'; +import { wrapInThemedTestApp } from '../../testUtils'; + +jest.mock('react-helmet', () => { + return ({ defaultTitle }) =>
defaultTitle: {defaultTitle}
; +}); + +describe('', () => { + it('should render with title', () => { + const rendered = render( + wrapInThemedTestApp(), + ); + rendered.getByText('Title'); + }); + + it('should render with titleComponent', () => { + const title = 'Custom title'; + const titleComponent = () =>

{title}

; + const rendered = render( + wrapInThemedTestApp(), + ); + rendered.getByText(title); + }); + + it('should render with description', () => { + const rendered = render( + wrapInThemedTestApp(), + ); + rendered.getByText('description'); + }); +}); diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.tsx new file mode 100644 index 0000000000..0f83f54c25 --- /dev/null +++ b/packages/core/src/layout/ContentHeader/ContentHeader.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODO favoriteable capability + */ + +import React, { ComponentType, Fragment, FC } from 'react'; +import { Typography, makeStyles } from '@material-ui/core'; +import Helmet from 'react-helmet'; +import { BackstageTheme } from '../../theme/theme'; + +const useStyles = makeStyles(theme => ({ + container: { + width: '100%', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-end', + alignItems: 'center', + }, + leftItemsBox: { + flex: '1 1 auto', + marginBottom: theme.spacing(1), + minWidth: 0, + overflow: 'visible', + }, + rightItemsBox: { + flex: '0 1 auto', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + marginLeft: theme.spacing(1), + marginBottom: theme.spacing(1), + minWidth: 0, + overflow: 'visible', + }, + description: {}, + title: { + display: 'inline-flex', + }, +})); + +type DefaultTitleProps = { + title?: string; + className: string; +}; + +const DefaultTitle: FC = ({ + title = 'Unknown page', + className, +}) => ( + + {title} + +); + +type ContentHeaderProps = { + title?: DefaultTitleProps['title']; + titleComponent?: ComponentType; + description?: string; +}; + +const ContentHeader: FC = ({ + description, + title, + titleComponent: TitleComponent = undefined, + children, +}) => { + const classes = useStyles(); + + const renderedTitle = TitleComponent ? ( + + ) : ( + + ); + return ( + + +
+
+ {renderedTitle} + {description && ( + + {description} + + )} +
+
{children}
+
+
+ ); +}; + +export default ContentHeader; diff --git a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx similarity index 96% rename from packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js rename to packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx index 1b90cad6f8..3a32d9371e 100644 --- a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js +++ b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.tsx @@ -41,7 +41,7 @@ describe('', () => { 'aria-disabled', 'true', ); - fireEvent.click(rendered.queryByText('Some label')); + fireEvent.click(rendered.queryByText('Some label') as Node); expect(onClickFunction).toHaveBeenCalled(); // We do not expect the dropdown to disappear after click expect(rendered.queryByText('Some label')).toBeInTheDocument(); @@ -84,7 +84,7 @@ describe('', () => { expect(onClickFunction).not.toHaveBeenCalled(); fireEvent.click(rendered.getByTestId('header-action-menu')); expect(onClickFunction).not.toHaveBeenCalled(); - fireEvent.click(rendered.queryByText('Secondary label')); + fireEvent.click(rendered.queryByText('Secondary label') as Node); expect(onClickFunction).toHaveBeenCalled(); // We do not expect the dropdown to disappear after click expect(rendered.queryByText('Some label')).toBeInTheDocument(); diff --git a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx similarity index 75% rename from packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js rename to packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx index 409338caeb..4799654cee 100644 --- a/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js +++ b/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Fragment } from 'react'; +import React, { Fragment, ReactElement, FC, ComponentType } from 'react'; import { IconButton, List, @@ -22,10 +22,20 @@ import { ListItemIcon, ListItemText, Popover, + ListItemTextProps, } from '@material-ui/core'; import { default as KebabMenuIcon } from './MenuVertical'; -const ActionItem = ({ +type ActionItemProps = { + label?: ListItemTextProps['primary']; + secondaryLabel?: ListItemTextProps['secondary']; + icon?: ReactElement; + disabled?: boolean; + onClick?: (event: React.MouseEvent) => void; + WrapperComponent?: ComponentType; +}; + +const ActionItem: FC = ({ label, secondaryLabel, icon, @@ -52,7 +62,11 @@ const ActionItem = ({ ); }; -const HeaderActionMenu = ({ actionItems }) => { +export type HeaderActionMenuProps = { + actionItems: ActionItemProps[]; +}; + +const HeaderActionMenu: FC = ({ actionItems }) => { const [open, setOpen] = React.useState(false); const anchorElRef = React.useRef(null); @@ -80,8 +94,10 @@ const HeaderActionMenu = ({ actionItems }) => { onClose={() => setOpen(false)} > - {actionItems.map(actionItem => { - return ; + {actionItems.map((actionItem, i) => { + return ( + + ); })} diff --git a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx similarity index 83% rename from packages/core/src/layout/HeaderLabel/HeaderLabel.test.js rename to packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx index 2d9e97bb51..727c890e35 100644 --- a/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js +++ b/packages/core/src/layout/HeaderLabel/HeaderLabel.test.tsx @@ -30,6 +30,13 @@ describe('', () => { expect(rendered.getByText('')).toBeInTheDocument(); }); + it('should say unknown when passing null as value prop', () => { + const rendered = render( + wrapInThemedTestApp(), + ); + expect(rendered.getByText('')).toBeInTheDocument(); + }); + it('should have value', () => { const rendered = render( wrapInThemedTestApp(), @@ -43,7 +50,7 @@ describe('', () => { , ), ); - const anchor = rendered.container.querySelector('a'); + const anchor = rendered.container.querySelector('a') as HTMLAnchorElement; expect(rendered.getByText('Value')).toBeInTheDocument(); expect(anchor.href).toBe('http://localhost/test'); }); diff --git a/packages/core/src/layout/HeaderLabel/HeaderLabel.js b/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx similarity index 50% rename from packages/core/src/layout/HeaderLabel/HeaderLabel.js rename to packages/core/src/layout/HeaderLabel/HeaderLabel.tsx index 23c3f9507a..4349399194 100644 --- a/packages/core/src/layout/HeaderLabel/HeaderLabel.js +++ b/packages/core/src/layout/HeaderLabel/HeaderLabel.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Typography, withStyles } from '@material-ui/core'; +import React, { FC } from 'react'; +import { Typography, makeStyles } from '@material-ui/core'; import { Link } from '@material-ui/core'; +import { BackstageTheme } from '../../theme/theme'; -const style = theme => ({ +const useStyles = makeStyles(theme => ({ root: { textAlign: 'left', margin: theme.spacing(2), @@ -40,27 +40,38 @@ const style = theme => ({ fontSize: 14, height: '16px', }, -}); +})); -class HeaderLabel extends Component { - static propTypes = { - label: PropTypes.string.isRequired, - value: PropTypes.node, - url: PropTypes.string, - }; +type HeaderLabelContentProps = { + value: React.ReactNode; + className: string; +}; - render() { - const { label, value, url, classes } = this.props; - const content = ( - {value || ''} - ); - return ( - - {label} - {url ? {content} : content} - - ); - } -} +const HeaderLabelContent: FC = ({ + value, + className, +}) => {value}; -export default withStyles(style)(HeaderLabel); +type HeaderLabelProps = { + label: string; + value?: HeaderLabelContentProps['value']; + url?: string; +}; + +const HeaderLabel: FC = ({ label, value, url }) => { + const classes = useStyles(); + const content = ( + '} + /> + ); + return ( + + {label} + {url ? {content} : content} + + ); +}; + +export default HeaderLabel;