UNPKG

decap-cms-core

Version:

Decap CMS core application, see decap-cms package for the main distribution.

298 lines (263 loc) 7.24 kB
import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; import { translate } from 'react-polyglot'; import { NavLink } from 'react-router-dom'; import { Icon, Dropdown, DropdownItem, StyledDropdownButton, colors, shadows, buttons, zIndex, } from 'decap-cms-ui-default'; import { connect } from 'react-redux'; import { SettingsDropdown } from '../UI'; import { checkBackendStatus } from '../../actions/status'; import { selectCanCreateNewEntry } from '../../reducers'; const styles = { buttonActive: css` color: ${colors.active}; `, }; function AppHeader(props) { return ( <header css={css` ${shadows.dropMain}; width: 100%; background-color: ${colors.foreground}; z-index: ${zIndex.zIndex300}; @media (min-height: 500px) { position: sticky; top: 0; } `} {...props} /> ); } const AppHeaderContent = styled.div` display: flex; flex-direction: column-reverse; padding: 0 12px; margin: 0 auto; @media (min-width: 800px) { max-width: 1440px; flex-direction: row; justify-content: space-between; } `; const AppHeaderButton = styled.button` ${buttons.button}; background: none; color: #7b8290; font-family: inherit; font-size: 13px; line-height: 1; font-weight: 500; display: inline-flex; flex-direction: column; gap: 2px; padding: 0 10px 10px; align-items: center; text-align: center; @media (min-width: 400px) { flex-direction: row; gap: 4px; } @media (min-width: 500px) { font-size: 16px; padding: 16px 20px; } ${Icon} { color: #b3b9c4; } &:hover, &:active, &:focus-visible { ${styles.buttonActive}; ${Icon} { ${styles.buttonActive}; } } ${props => css` &.${props.activeClassName} { ${styles.buttonActive}; ${Icon} { ${styles.buttonActive}; } } `}; `; const AppHeaderNavLink = AppHeaderButton.withComponent(NavLink); const AppHeaderActions = styled.div` display: flex; align-items: center; justify-content: space-between; `; const AppHeaderQuickNewButton = styled(StyledDropdownButton)` ${buttons.button}; ${buttons.medium}; ${buttons.gray}; white-space: nowrap; margin-right: 8px; &:after { top: 11px; } `; const AppHeaderNavList = styled.ul` display: flex; margin: 0; list-style: none; justify-content: space-around; @media (min-width: 800px) { justify-content: flex-start; } `; const AppHeaderLogo = styled.li` display: flex; align-items: center; img { padding: 12px 20px; max-height: 56px; max-width: 300px; object-fit: contain; object-position: center; } `; class Header extends React.Component { static propTypes = { user: PropTypes.object.isRequired, collections: ImmutablePropTypes.map.isRequired, creatableCollections: ImmutablePropTypes.list.isRequired, onCreateEntryClick: PropTypes.func.isRequired, onLogoutClick: PropTypes.func.isRequired, openMediaLibrary: PropTypes.func.isRequired, hasWorkflow: PropTypes.bool.isRequired, displayUrl: PropTypes.string, logoUrl: PropTypes.string, // Deprecated, replaced by `logo.src` logo: PropTypes.shape({ src: PropTypes.string.isRequired, show_in_header: PropTypes.bool, }), isTestRepo: PropTypes.bool, t: PropTypes.func.isRequired, checkBackendStatus: PropTypes.func.isRequired, }; intervalId; componentDidMount() { // Manually validate PropTypes - React 19 breaking change PropTypes.checkPropTypes(Header.propTypes, this.props, 'prop', 'Header'); this.intervalId = setInterval(() => { this.props.checkBackendStatus(); }, 5 * 60 * 1000); } componentWillUnmount() { clearInterval(this.intervalId); } handleCreatePostClick = collectionName => { const { onCreateEntryClick } = this.props; if (onCreateEntryClick) { onCreateEntryClick(collectionName); } }; render() { const { user, creatableCollections, onLogoutClick, openMediaLibrary, hasWorkflow, displayUrl, logoUrl, // Deprecated, replaced by `logo.src` logo, isTestRepo, t, showMediaButton, } = this.props; const shouldShowLogo = logo?.show_in_header && logo?.src; return ( <AppHeader> <AppHeaderContent> <nav> <AppHeaderNavList> {shouldShowLogo && ( <AppHeaderLogo> <img src={logo?.src || logoUrl} alt="Logo" /> </AppHeaderLogo> )} <li> <AppHeaderNavLink to="/" activeClassName="header-link-active" isActive={(match, location) => location.pathname.startsWith('/collections/')} > <Icon type="page" /> {t('app.header.content')} </AppHeaderNavLink> </li> {hasWorkflow && ( <li> <AppHeaderNavLink to="/workflow" activeClassName="header-link-active"> <Icon type="workflow" /> {t('app.header.workflow')} </AppHeaderNavLink> </li> )} {showMediaButton && ( <li> <AppHeaderButton onClick={openMediaLibrary}> <Icon type="media-alt" /> {t('app.header.media')} </AppHeaderButton> </li> )} </AppHeaderNavList> </nav> <AppHeaderActions> {creatableCollections.size > 0 && ( <Dropdown renderButton={() => ( <AppHeaderQuickNewButton> {t('app.header.quickAdd')}</AppHeaderQuickNewButton> )} dropdownTopOverlap="30px" dropdownWidth="160px" dropdownPosition="left" > {creatableCollections.map(collection => ( <DropdownItem key={collection.get('name')} label={collection.get('label_singular') || collection.get('label')} onClick={() => this.handleCreatePostClick(collection.get('name'))} /> ))} </Dropdown> )} <SettingsDropdown displayUrl={displayUrl} isTestRepo={isTestRepo} imageUrl={user?.avatar_url} onLogoutClick={onLogoutClick} /> </AppHeaderActions> </AppHeaderContent> </AppHeader> ); } } const mapDispatchToProps = { checkBackendStatus, }; function mapStateToProps(state, ownProps) { return { creatableCollections: ownProps.collections .filter(collection => selectCanCreateNewEntry(state, collection.get('name'))) .toList(), }; } export default connect(mapStateToProps, mapDispatchToProps)(translate()(Header));