mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Init refacto menu
Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
parent
be03d496d8
commit
a2123bdc7b
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user