Init refacto menu

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2021-06-16 09:47:50 +02:00
parent be03d496d8
commit a2123bdc7b
14 changed files with 86 additions and 297 deletions

View File

@ -44,6 +44,7 @@ class StrapiApp {
this.reducers = reducers;
this.translations = translations;
this.hooksDict = {};
this.menu = [];
this.settings = {
global: {
id: 'global',
@ -72,6 +73,24 @@ class StrapiApp {
}
};
addMenuLink = link => {
const stringifiedLink = JSON.stringify(link);
invariant(link.to, `link.to should be defined for ${stringifiedLink}`);
invariant(
typeof link.to === 'string',
`Expected link.to to be a string instead received ${typeof link.to}`
);
invariant(
link.intlLabel?.id && link.intlLabel?.defaultMessage,
`link.intlLabel.id & link.intlLabel.defaultMessage for ${stringifiedLink}`
);
invariant(
link.Component && typeof link.Component === 'function',
`link.Component should be a valid React Component`
);
};
addMiddlewares = middlewares => {
middlewares.forEach(middleware => {
this.middlewares.add(middleware);
@ -117,6 +136,7 @@ class StrapiApp {
this.appPlugins[plugin].register({
addComponents: this.addComponents,
addFields: this.addFields,
addMenuLink: this.addMenuLink,
addMiddlewares: this.addMiddlewares,
addReducers: this.addReducers,
createSettingSection: this.createSettingSection,

View File

@ -1,11 +0,0 @@
import styled from 'styled-components';
const Search = styled.input`
width: 100%;
padding: 0 15px;
outline: 0;
font-size: 1.1rem;
color: ${({ theme }) => theme.main.colors.white};
`;
export default Search;

View File

@ -1,11 +0,0 @@
import styled from 'styled-components';
const SearchButton = styled.button`
padding: 0 10px;
line-height: normal;
&:focus {
outline: 0;
}
`;
export default SearchButton;

View File

@ -1,11 +0,0 @@
import styled from 'styled-components';
const SearchWrapper = styled.div`
display: flex;
width: 100%;
height: 19px;
justify-content: space-between;
border-bottom: 1px solid;
`;
export default SearchWrapper;

View File

@ -1,81 +0,0 @@
import React, { useState, createRef, useEffect } from 'react';
import { camelCase } from 'lodash';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO remove this
import messages from './messages.json';
import Search from './Search';
import Title from './Title';
import SearchButton from './SearchButton';
import SearchWrapper from './SearchWrapper';
const LeftMenuLinkHeader = ({ section, searchable, setSearch, search }) => {
const [showSearch, setShowSearch] = useState(false);
const ref = createRef();
const { id, defaultMessage } = messages[camelCase(section)];
useEffect(() => {
if (showSearch && ref.current) {
ref.current.focus();
}
}, [ref, showSearch]);
const toggleSearch = () => {
setShowSearch(prev => !prev);
};
const handleChange = ({ target: { value } }) => {
setSearch(value);
};
const clearSearch = () => {
setSearch('');
setShowSearch(false);
};
return (
<Title>
{!showSearch ? (
<>
<FormattedMessage id={id} defaultMessage={defaultMessage} />
{searchable && (
<SearchButton onClick={toggleSearch}>
<FontAwesomeIcon icon="search" />
</SearchButton>
)}
</>
) : (
<SearchWrapper>
<div>
<FontAwesomeIcon style={{ fontSize: 12 }} icon="search" />
</div>
<FormattedMessage id="components.Search.placeholder">
{message => (
<Search ref={ref} onChange={handleChange} value={search} placeholder={message} />
)}
</FormattedMessage>
<SearchButton onClick={clearSearch}>
<FontAwesomeIcon icon="times" />
</SearchButton>
</SearchWrapper>
)}
</Title>
);
};
LeftMenuLinkHeader.propTypes = {
section: PropTypes.string.isRequired,
searchable: PropTypes.bool,
setSearch: PropTypes.func,
search: PropTypes.string,
};
LeftMenuLinkHeader.defaultProps = {
search: null,
searchable: false,
setSearch: () => {},
};
export default LeftMenuLinkHeader;

View File

@ -1,38 +0,0 @@
{
"collectionType": {
"id": "app.components.LeftMenuLinkContainer.collectionTypes",
"defaultMessage": "Collection Types"
},
"singleType": {
"id": "app.components.LeftMenuLinkContainer.singleTypes",
"defaultMessage": "Single Types"
},
"listPlugins": {
"id": "app.components.LeftMenuLinkContainer.listPlugins",
"defaultMessage": "Plugins"
},
"installNewPlugin": {
"id": "app.components.LeftMenuLinkContainer.installNewPlugin",
"defaultMessage": "Marketplace"
},
"configuration": {
"id": "app.components.LeftMenuLinkContainer.configuration",
"defaultMessage": "Configurations"
},
"plugins": {
"id": "app.components.LeftMenuLinkContainer.plugins",
"defaultMessage": "Plugins"
},
"general": {
"id": "app.components.LeftMenuLinkContainer.general",
"defaultMessage": "General"
},
"noPluginsInstalled": {
"id": "app.components.LeftMenuLinkContainer.noPluginsInstalled",
"defaultMessage": "No plugins installed yet"
},
"settings": {
"id": "app.components.LeftMenuLinkContainer.settings",
"defaultMessage": "Settings"
}
}

View File

@ -1,84 +1,33 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import matchSorter from 'match-sorter';
import { sortBy } from 'lodash';
import { FormattedMessage } from 'react-intl';
import LeftMenuLink from '../Link';
import LeftMenuLinkHeader from '../LinkHeader';
import LeftMenuListLink from './LeftMenuListLink';
import EmptyLinksList from './EmptyLinksList';
import EmptyLinksListWrapper from './EmptyLinksListWrapper';
const LeftMenuLinksSection = ({
section,
searchable,
location,
links,
emptyLinksListMessage,
shrink,
}) => {
const [search, setSearch] = useState('');
const filteredList = sortBy(
matchSorter(links, search, {
keys: ['label'],
}),
'label'
);
const LeftMenuLinksSection = ({ location, links }) => {
return (
<>
<LeftMenuLinkHeader
section={section}
searchable={searchable}
setSearch={setSearch}
search={search}
/>
<LeftMenuListLink shrink={shrink}>
{filteredList.length > 0 ? (
filteredList.map((link, index) => (
<LeftMenuLink
location={location}
// There is no id or unique value in the link object for the moment.
// eslint-disable-next-line react/no-array-index-key
key={index}
iconName={link.icon}
label={link.label}
destination={link.destination}
notificationsCount={link.notificationsCount || 0}
search={link.search}
/>
))
) : (
<EmptyLinksListWrapper>
<FormattedMessage id={emptyLinksListMessage} defaultMessage="No plugins installed yet">
{msg => <EmptyLinksList>{msg}</EmptyLinksList>}
</FormattedMessage>
</EmptyLinksListWrapper>
)}
<LeftMenuListLink>
{links.map(link => (
<LeftMenuLink
location={location}
key={link.destination}
iconName={link.icon}
label={link.label}
destination={link.destination}
notificationsCount={link.notificationsCount || 0}
search={link.search}
/>
))}
</LeftMenuListLink>
</>
);
};
LeftMenuLinksSection.defaultProps = {
shrink: false,
};
LeftMenuLinksSection.propTypes = {
section: PropTypes.string.isRequired,
searchable: PropTypes.bool.isRequired,
shrink: PropTypes.bool,
location: PropTypes.shape({
pathname: PropTypes.string,
}).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
emptyLinksListMessage: PropTypes.string,
};
LeftMenuLinksSection.defaultProps = {
emptyLinksListMessage: 'components.ListRow.empty',
};
export default LeftMenuLinksSection;

View File

@ -1,13 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1140;
background: ${({ theme }) => theme.main.colors.white};
`;
export default Wrapper;

View File

@ -1,34 +0,0 @@
/*
*
* This component is used to show a global loader while permissions are being checked
* it prevents from lifting the state up in order to avoid setting more logic into the Admin container
* this way we can show a global loader without modifying the Admin code
*
*/
import React from 'react';
import { createPortal } from 'react-dom';
import { LoadingIndicatorPage } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import Wrapper from './Wrapper';
const MOUNT_NODE = document.getElementById('app') || document.createElement('div');
const Loader = ({ show }) => {
if (show) {
return createPortal(
<Wrapper>
<LoadingIndicatorPage />
</Wrapper>,
MOUNT_NODE
);
}
return null;
};
Loader.propTypes = {
show: PropTypes.bool.isRequired,
};
export default Loader;

View File

@ -1,6 +1,6 @@
import styled from 'styled-components';
const Title = styled.div`
const SectionTitle = styled.div`
display: flex;
justify-content: space-between;
padding-left: 2rem;
@ -15,7 +15,7 @@ const Title = styled.div`
max-height: 26px;
`;
Title.defaultProps = {
SectionTitle.defaultProps = {
theme: {
main: {
colors: {
@ -27,4 +27,4 @@ Title.defaultProps = {
},
};
export default Title;
export default SectionTitle;

View File

@ -2,4 +2,3 @@ export { default as Footer } from './Footer';
export { default as Header } from './Header';
export { default as LinksContainer } from './Links';
export { default as LinksSection } from './LinksSection';
export { default as Loader } from './Loader';

View File

@ -1,42 +1,44 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { useAppInfos, useStrapiApp } from '@strapi/helper-plugin';
import { Footer, Header, Loader, LinksContainer, LinksSection } from './compos';
import SectionTitle from './compos/SectionTitle';
import { Footer, Header, LinksContainer, LinksSection } from './compos';
import Wrapper from './Wrapper';
import useMenuSections from './useMenuSections';
const LeftMenu = () => {
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
const location = useLocation();
const { shouldUpdateStrapi } = useAppInfos();
const { plugins } = useStrapiApp();
const { isLoading, generalSectionLinks, pluginsSectionLinks } = useMenuSections(
plugins,
shouldUpdateStrapi
);
return (
<Wrapper>
<Loader show={isLoading} />
<Header />
<LinksContainer>
{pluginsSectionLinks.length > 0 && (
<LinksSection
section="plugins"
name="plugins"
links={pluginsSectionLinks}
location={location}
searchable={false}
emptyLinksListMessage="app.components.LeftMenuLinkContainer.noPluginsInstalled"
/>
<>
<SectionTitle>
<FormattedMessage
id="app.components.LeftMenuLinkContainer.listPlugins"
defaultMessage="Plugins"
/>
</SectionTitle>
<LinksSection
links={pluginsSectionLinks}
location={location}
searchable={false}
emptyLinksListMessage="app.components.LeftMenuLinkContainer.noPluginsInstalled"
/>
</>
)}
{generalSectionLinks.length > 0 && (
<LinksSection
section="general"
name="general"
links={generalSectionLinks}
location={location}
searchable={false}
/>
<>
<SectionTitle>
<FormattedMessage
id="app.components.LeftMenuLinkContainer.general"
defaultMessage="General"
/>
</SectionTitle>
<LinksSection links={generalSectionLinks} location={location} searchable={false} />
</>
)}
</LinksContainer>
<Footer key="footer" />
@ -44,4 +46,9 @@ const LeftMenu = () => {
);
};
LeftMenu.propTypes = {
generalSectionLinks: PropTypes.array.isRequired,
pluginsSectionLinks: PropTypes.array.isRequired,
};
export default memo(LeftMenu);

View File

@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { useRBACProvider } from '@strapi/helper-plugin';
import { useRBACProvider, useAppInfos, useStrapiApp } from '@strapi/helper-plugin';
import { useSelector, useDispatch } from 'react-redux';
import getPluginSectionLinks from './utils/getPluginSectionLinks';
import getGeneralLinks from './utils/getGeneralLinks';
@ -7,10 +7,12 @@ import { setSectionLinks, unsetIsLoading } from './actions';
import toPluginLinks from './utils/toPluginLinks';
import selectMenuLinks from './selectors';
const useMenuSections = (plugins, shouldUpdateStrapi) => {
const useMenuSections = () => {
const state = useSelector(selectMenuLinks);
const dispatch = useDispatch();
const { allPermissions } = useRBACProvider();
const { shouldUpdateStrapi } = useAppInfos();
const { plugins } = useStrapiApp();
// We are using a ref because we don't want our effect to have this in its dependencies array
const generalSectionLinksRef = useRef(state.generalSectionLinks);

View File

@ -12,6 +12,9 @@ import adminPermissions from '../../permissions';
import Header from '../../components/Header/index';
import NavTopRightWrapper from '../../components/NavTopRightWrapper';
import LeftMenu from '../../components/LeftMenu';
// TODO
import useMenuSections from '../../components/LeftMenu/useMenuSections';
import Onboarding from '../../components/Onboarding';
import { useReleaseNotification } from '../../hooks';
import Logout from './Logout';
@ -64,10 +67,18 @@ const Admin = () => {
// Show a notification when the current version of Strapi is not the latest one
useReleaseNotification();
useTrackUsage();
const { isLoading, generalSectionLinks, pluginsSectionLinks } = useMenuSections();
if (isLoading) {
return <LoadingIndicatorPage />;
}
return (
<Wrapper>
<LeftMenu />
<LeftMenu
generalSectionLinks={generalSectionLinks}
pluginsSectionLinks={pluginsSectionLinks}
/>
<NavTopRightWrapper>
{/* Injection zone not ready yet */}
<Logout />