Reskin and refacto the left admin menu and add Single type section

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2020-02-07 14:46:12 +01:00
parent de9521ce6b
commit 4f5cd5a209
16 changed files with 533 additions and 336 deletions

View File

@ -6,6 +6,7 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { PropTypes } from 'prop-types';
import LeftMenuLink from '../LeftMenuLink';
import Wrapper from './Wrapper';
@ -13,7 +14,8 @@ import messages from './messages.json';
defineMessages(messages);
function LeftMenuFooter({ version, ...rest }) {
const LeftMenuFooter = ({ version }) => {
const location = useLocation();
const staticLinks = [
{
icon: 'book',
@ -32,15 +34,20 @@ function LeftMenuFooter({ version, ...rest }) {
<ul className="list">
{staticLinks.map(link => (
<LeftMenuLink
{...rest}
{...link}
location={location}
iconName={link.icon}
label={messages[link.label].id}
key={link.label}
destination={link.destination}
/>
))}
</ul>
<div className="poweredBy">
<FormattedMessage {...messages.poweredBy} key="poweredBy" />
<FormattedMessage
id={messages.poweredBy.id}
defaultMessage={messages.poweredBy.defaultMessage}
key="poweredBy"
/>
<a
key="website"
href="https://strapi.io"
@ -61,7 +68,7 @@ function LeftMenuFooter({ version, ...rest }) {
</div>
</Wrapper>
);
}
};
LeftMenuFooter.propTypes = {
version: PropTypes.string.isRequired,

View File

@ -1,22 +1,14 @@
/**
*
* LeftMenuHeader
*
*/
import React from 'react';
import { Link } from 'react-router-dom';
import Wrapper from './Wrapper';
function LeftMenuHeader() {
return (
<Wrapper>
<Link to="/" className="leftMenuHeaderLink">
<span className="projectName" />
</Link>
</Wrapper>
);
}
const LeftMenuHeader = () => (
<Wrapper>
<Link to="/" className="leftMenuHeaderLink">
<span className="projectName" />
</Link>
</Wrapper>
);
export default LeftMenuHeader;

View File

@ -0,0 +1,39 @@
import styled from 'styled-components';
const A = styled.a`
position: relative;
padding-top: 0.8rem;
padding-bottom: 0.2rem;
padding-left: 1.6rem;
min-height: 3.6rem;
border-left: 0.3rem solid transparent;
cursor: pointer;
color: ${props => props.theme.main.colors.leftMenu['link-color']};
text-decoration: none;
display: block;
-webkit-font-smoothing: antialiased;
&:hover {
color: ${props => props.theme.main.colors.white};
background: ${props => props.theme.main.colors.leftMenu['link-hover']};
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
text-decoration: none;
}
&:focus {
color: ${props => props.theme.main.colors.white};
text-decoration: none;
}
&:visited {
color: ${props => props.theme.main.colors.leftMenu['link-color']};
}
&.linkActive {
color: white !important;
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
}
`;
export default A;

View File

@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const FaIcon = styled(({ small, ...props }) => <FontAwesomeIcon {...props} />)`
position: absolute;
top: calc(50% - 0.9rem + 0.5rem);
left: 1.6rem;
margin-right: 1.2rem;
font-size: ${props => (props.small ? '1rem' : '1.4rem')};
width: 1.4rem;
padding-bottom: 0.2rem;
text-align: center;
`;
const LeftMenuIcon = ({ icon }) => (
<FaIcon small={icon === 'circle'} icon={icon} />
);
LeftMenuIcon.propTypes = {
icon: PropTypes.string,
};
LeftMenuIcon.defaultProps = {
icon: 'circle',
};
export default LeftMenuIcon;

View File

@ -0,0 +1,96 @@
/**
*
* LeftMenuLink
*
*/
import React from 'react';
import { startsWith } from 'lodash';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styled from 'styled-components';
import { Link, withRouter } from 'react-router-dom';
import en from '../../translations/en.json';
import LeftMenuIcon from './LeftMenuIcon';
import A from './A';
const LinkLabel = styled.span`
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
`;
const LeftMenuLinkContent = ({
destination,
iconName,
label,
location,
source,
suffixUrlToReplaceForLeftMenuHighlight,
}) => {
const isLinkActive = startsWith(
location.pathname.replace('/admin', '').concat('/'),
destination.replace(suffixUrlToReplaceForLeftMenuHighlight, '').concat('/')
);
// Check if messageId exists in en locale to prevent warning messages
const content = en[label] ? (
<FormattedMessage
id={label}
defaultMessage="{label}"
values={{
label: `${label}`,
}}
>
{message => <LinkLabel>{message}</LinkLabel>}
</FormattedMessage>
) : (
<LinkLabel>{label}</LinkLabel>
);
// Create external or internal link.
return destination.includes('http') ? (
<A
className={isLinkActive ? 'linkActive' : ''}
href={destination}
target="_blank"
rel="noopener noreferrer"
>
<LeftMenuIcon icon={iconName} />
{content}
</A>
) : (
<A
as={Link}
className={isLinkActive ? 'linkActive' : ''}
to={{
pathname: destination,
search: source ? `?source=${source}` : '',
}}
>
<LeftMenuIcon icon={iconName} />
{content}
</A>
);
};
LeftMenuLinkContent.propTypes = {
destination: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
location: PropTypes.shape({
pathname: PropTypes.string,
}).isRequired,
source: PropTypes.string,
suffixUrlToReplaceForLeftMenuHighlight: PropTypes.string,
};
LeftMenuLinkContent.defaultProps = {
source: '',
suffixUrlToReplaceForLeftMenuHighlight: '',
};
export default withRouter(LeftMenuLinkContent);

View File

@ -1,129 +0,0 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
const Li = styled.li`
position: relative;
overflow: hidden;
&.dotted-link {
background: red;
}
&:not(:first-child) {
margin-top: 0;
}
.plugin {
cursor: pointer;
position: absolute;
top: 10px;
left: calc(100% - 4px);
display: inline-block;
width: auto;
height: 20px;
transition: right 1s ease-in-out;
span {
display: inline-block;
overflow: hidden;
width: auto;
height: 20px;
padding: 0 14px 0 10px;
color: #ffffff;
font-size: 12px;
line-height: 20px;
background: #0097f7;
border-radius: 3px;
transition: transform 0.3s ease-in-out;
white-space: pre;
&:hover {
transform: translateX(calc(-100% + 9px));
}
}
}
.link {
position: relative;
padding-top: 0.8rem;
padding-bottom: 0.2rem;
padding-left: 1.6rem;
min-height: 3.6rem;
border-left: 0.3rem solid transparent;
cursor: pointer;
color: ${props => props.theme.main.colors.leftMenu['link-color']};
text-decoration: none;
display: block;
-webkit-font-smoothing: antialiased;
&:hover {
color: ${props => props.theme.main.colors.white};
background: ${props => props.theme.main.colors.leftMenu['link-hover']};
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
text-decoration: none;
}
&:focus {
color: ${props => props.theme.main.colors.white};
text-decoration: none;
}
&:visited {
color: ${props => props.theme.main.colors.leftMenu['link-color']};
}
span {
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.linkActive {
color: white !important;
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
}
.linkIcon {
position: absolute;
top: calc(50% - 0.9rem + 0.5rem);
left: 1.6rem;
margin-right: 1.2rem;
font-size: 1.4rem;
width: 1.4rem;
padding-bottom: 0.2rem;
text-align: center;
}
.linkLabel {
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
}
`;
Li.defaultProps = {
theme: {
main: {
colors: {
leftMenu: {},
strapi: {},
},
sizes: {
header: {},
leftMenu: {},
},
},
},
};
Li.propTypes = {
theme: PropTypes.object,
};
export default Li;

View File

@ -0,0 +1,33 @@
import styled from 'styled-components';
const Plugin = styled.div`
cursor: pointer;
position: absolute;
top: 10px;
left: calc(100% - 4px);
display: inline-block;
width: auto;
height: 20px;
transition: right 1s ease-in-out;
span {
display: inline-block;
overflow: hidden;
width: auto;
height: 20px;
padding: 0 14px 0 10px;
color: #ffffff;
font-size: 12px;
line-height: 20px;
background: #0097f7;
border-radius: 3px;
transition: transform 0.3s ease-in-out;
white-space: pre;
&:hover {
transform: translateX(calc(-100% + 9px));
}
}
`;
export default Plugin;

View File

@ -5,98 +5,59 @@
*/
import React from 'react';
import { startsWith, upperFirst } from 'lodash';
import { upperFirst } from 'lodash';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import en from '../../translations/en.json';
import Li from './Li';
/* eslint-disable */
function LeftMenuLink(props) {
const isLinkActive = startsWith(
props.location.pathname.replace('/admin', '').concat('/'),
props.destination
.replace(props.suffixUrlToReplaceForLeftMenuHighlight, '')
.concat('/')
);
import LeftMenuLinkContent from './LeftMenuLinkContent';
import Plugin from './Plugin';
const LeftMenuLink = ({
destination,
iconName,
label,
location,
source,
suffixUrlToReplaceForLeftMenuHighlight,
}) => {
const plugin =
props.source !== 'content-manager' && props.source !== '' ? (
<div className="plugin">
<span>{upperFirst(props.source.split('-').join(' '))}</span>
</div>
source !== 'content-manager' && source !== '' ? (
<Plugin>
<span>{upperFirst(source.split('-').join(' '))}</span>
</Plugin>
) : (
''
);
// Check if messageId exists in en locale to prevent warning messages
const content = en[props.label] ? (
<FormattedMessage
id={props.label}
defaultMessage="{label}"
values={{
label: `${props.label}`,
}}
className="linkLabel"
/>
) : (
<span className="linkLabel">{props.label}</span>
);
// Icon.
const icon = <FontAwesomeIcon className={`linkIcon`} icon={props.icon} />;
// Create external or internal link.
const link = props.destination.includes('http') ? (
<a
className={`link ${isLinkActive ? 'linkActive' : ''}`}
href={props.destination}
target="_blank"
rel="noopener noreferrer"
>
{icon}
{content}
</a>
) : (
<Link
className={`link ${isLinkActive ? 'linkActive' : ''}`}
to={{
pathname: props.destination,
search: props.source ? `?source=${props.source}` : '',
}}
>
{icon}
{content}
</Link>
);
return (
<Li>
{link}
<>
<LeftMenuLinkContent
destination={destination}
iconName={iconName}
label={label}
location={location}
source={source}
suffixUrlToReplaceForLeftMenuHighlight={
suffixUrlToReplaceForLeftMenuHighlight
}
/>
{plugin}
</Li>
</>
);
}
};
LeftMenuLink.propTypes = {
destination: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconName: PropTypes.string,
label: PropTypes.string.isRequired,
location: PropTypes.shape({
pathname: PropTypes.string,
}).isRequired,
pluginSuffixUrl: PropTypes.string,
source: PropTypes.string,
suffixUrlToReplaceForLeftMenuHighlight: PropTypes.string,
};
LeftMenuLink.defaultProps = {
pluginSuffixUrl: '',
iconName: 'circle',
source: '',
suffixUrlToReplaceForLeftMenuHighlight: '',
};

View File

@ -1,27 +1,24 @@
/**
*
* LeftMenuLinkContainer
*
*/
import React from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { get, snakeCase, isEmpty, map, sortBy } from 'lodash';
import { get, snakeCase, isEmpty } from 'lodash';
import { SETTINGS_BASE_URL } from '../../config';
import LeftMenuLink from '../LeftMenuLink';
import Wrapper from './Wrapper';
import messages from './messages.json';
/* eslint-disable */
import LeftMenuLinkSection from '../LeftMenuLinkSection';
function LeftMenuLinkContainer({ plugins, ...rest }) {
// Generate the list of sections
const pluginsSections = Object.keys(plugins).reduce((acc, current) => {
const LeftMenuLinkContainer = ({ plugins }) => {
const location = useLocation();
// Generate the list of content types sections
const contentTypesSections = Object.keys(plugins).reduce((acc, current) => {
plugins[current].leftMenuSections.forEach((section = {}) => {
if (!isEmpty(section.links)) {
acc[snakeCase(section.name)] = {
name: section.name,
searchable: true,
links: get(acc[snakeCase(section.name)], 'links', []).concat(
section.links
.filter(link => link.isDisplayed !== false)
@ -40,109 +37,70 @@ function LeftMenuLinkContainer({ plugins, ...rest }) {
return acc;
}, {});
const linkSections = Object.keys(pluginsSections).map((current, j) => {
const contentTypes = pluginsSections[current].links;
return (
<div key={j}>
<p className="title">
<FormattedMessage {...messages.contentTypes}>
{title => title}
</FormattedMessage>
</p>
<ul className="list models-list">
{sortBy(contentTypes, 'label').map((link, i) => (
<LeftMenuLink
{...rest}
key={`${i}-${link.label}`}
icon={link.icon || 'circle'}
label={link.label}
destination={`/plugins/${link.plugin}/${link.destination ||
link.uid}`}
/>
))}
</ul>
</div>
);
});
// Check if the plugins list is empty or not and display plugins by name
const pluginsLinks = !isEmpty(plugins) ? (
map(sortBy(plugins, 'name'), plugin => {
const shouldInjectPlugin = !!plugin.mainComponent;
if (
// Generate the list of plugin links
const pluginsLinks = Object.values(plugins)
.filter(
plugin =>
plugin.id !== 'email' &&
plugin.id !== 'content-manager' &&
shouldInjectPlugin
) {
const pluginSuffixUrl = plugin.suffixUrl
? plugin.suffixUrl(plugins)
: '';
!!plugin.mainComponent
)
.map(plugin => {
const pluginSuffixUrl = plugin.suffixUrl ? plugin.suffixUrl(plugins) : '';
const destination = `/plugins/${get(plugin, 'id')}${pluginSuffixUrl}`;
return {
icon: get(plugin, 'icon') || 'plug',
label: get(plugin, 'name'),
destination: `/plugins/${get(plugin, 'id')}${pluginSuffixUrl}`,
};
});
return (
<LeftMenuLink
{...rest}
key={get(plugin, 'id')}
icon={get(plugin, 'icon') || 'plug'}
label={get(plugin, 'name')}
destination={destination}
pluginSuffixUrl={pluginSuffixUrl}
suffixUrlToReplaceForLeftMenuHighlight={
plugin.suffixUrlToReplaceForLeftMenuHighlight || ''
}
/>
);
}
})
) : (
<li key="emptyList" className="noPluginsInstalled">
<FormattedMessage {...messages.noPluginsInstalled} key="noPlugins" />.
</li>
);
const staticLinks = [
{
icon: 'list',
label: messages.listPlugins.id,
destination: '/list-plugins',
const menu = {
...contentTypesSections,
plugins: {
searchable: false,
name: 'plugins',
emptyLinksListMessage: messages.noPluginsInstalled.id,
links: pluginsLinks,
},
{
icon: 'shopping-basket',
label: messages.installNewPlugin.id,
destination: '/marketplace',
general: {
searchable: false,
name: 'general',
links: [
{
icon: 'list',
label: messages.listPlugins.id,
destination: '/list-plugins',
},
{
icon: 'shopping-basket',
label: messages.installNewPlugin.id,
destination: '/marketplace',
},
{
icon: 'cog',
label: messages.settings.id,
destination: SETTINGS_BASE_URL,
},
],
},
{
icon: 'cog',
label: messages.settings.id,
destination: SETTINGS_BASE_URL,
},
];
};
return (
<Wrapper>
{linkSections}
<div>
<p className="title">
<FormattedMessage {...messages.plugins} />
</p>
<ul className="list">{pluginsLinks}</ul>
</div>
<div>
<p className="title">
<FormattedMessage {...messages.general} />
</p>
<ul className="list">
{staticLinks.map(link => (
<LeftMenuLink {...rest} key={link.destination} {...link} />
))}
</ul>
</div>
{Object.keys(menu).map(current => (
<LeftMenuLinkSection
key={current}
links={menu[current].links}
section={current}
location={location}
searchable={menu[current].searchable}
emptyLinksListMessage={menu[current].emptyLinksListMessage}
/>
))}
</Wrapper>
);
}
};
LeftMenuLinkContainer.propTypes = {
plugins: PropTypes.object.isRequired,

View File

@ -3,6 +3,10 @@
"id": "app.components.LeftMenuLinkContainer.contentTypes",
"defaultMessage": "Collection Types"
},
"singleTypes": {
"id": "app.components.LeftMenuLinkContainer.singleTypes",
"defaultMessage": "Single Types"
},
"listPlugins": {
"id": "app.components.LeftMenuLinkContainer.listPlugins",
"defaultMessage": "Plugins"

View File

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

View File

@ -0,0 +1,92 @@
import React, { useState, createRef, useEffect } from 'react';
import { camelCase } from 'lodash';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import messages from '../LeftMenuLinkContainer/messages.json';
import Search from './Search';
const Title = styled.div`
display: flex;
justify-content: space-between;
padding-left: 2rem;
padding-right: 1.6rem;
padding-top: 0.7rem;
margin-bottom: 0.8rem;
color: ${props => props.theme.main.colors.leftMenu['title-color']};
text-transform: uppercase;
font-size: 1.1rem;
letter-spacing: 0.1rem;
font-weight: 800;
`;
const SearchButton = styled.button`
padding: 0 10px;
`;
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 !showSearch ? (
<Title>
<FormattedMessage id={id} defaultMessage={defaultMessage} />
{searchable && (
<SearchButton onClick={toggleSearch}>
<FontAwesomeIcon icon="search" />
</SearchButton>
)}
</Title>
) : (
<Title>
<div>
<FontAwesomeIcon icon="search" />
</div>
<Search
ref={ref}
onChange={handleChange}
value={search}
placeholder="search…"
/>
<SearchButton onClick={clearSearch}>
<FontAwesomeIcon icon="times" />
</SearchButton>
</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

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import matchSorter from 'match-sorter';
import styled from 'styled-components';
import { sortBy } from 'lodash';
import { FormattedMessage } from 'react-intl';
import LeftMenuLink from '../LeftMenuLink';
import LeftMenuLinkHeader from '../LeftMenuLinkHeader';
const LeftMenuListLink = styled.div`
max-height: 180px;
overflow: auto;
`;
const EmptyLinksList = styled.div`
color: ${props => props.theme.main.colors.white};
padding-left: 1.6rem;
padding-right: 1.6rem;
font-weight: 300;
min-height: 3.6rem;
padding-top: 0.2rem;
`;
const LeftMenuLinksSection = ({
section,
searchable,
location,
links,
emptyLinksListMessage,
}) => {
const [search, setSearch] = useState('');
const filteredList = sortBy(
matchSorter(links, search, {
keys: ['label'],
}),
'label'
);
const getLinkDestination = link => {
return ['plugins', 'general'].includes(section)
? link.destination
: `/plugins/${link.plugin}/${link.destination || link.uid}`;
};
return (
<>
<LeftMenuLinkHeader
section={section}
searchable={searchable}
setSearch={setSearch}
search={search}
/>
<LeftMenuListLink>
{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={getLinkDestination(link)}
/>
))
) : (
<EmptyLinksList>
<FormattedMessage
id={emptyLinksListMessage}
defaultMessage="No plugins installed yet"
/>
</EmptyLinksList>
)}
</LeftMenuListLink>
</>
);
};
LeftMenuLinksSection.propTypes = {
section: PropTypes.string.isRequired,
searchable: PropTypes.bool.isRequired,
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

@ -5,20 +5,23 @@
*/
import React from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import LeftMenuHeader from '../../components/LeftMenuHeader';
import LeftMenuLinkContainer from '../../components/LeftMenuLinkContainer';
import LeftMenuFooter from '../../components/LeftMenuFooter';
import Wrapper from './Wrapper';
function LeftMenu(props) {
return (
<Wrapper>
<LeftMenuHeader key="header" {...props} />
<LeftMenuLinkContainer {...props} />
<LeftMenuFooter key="footer" {...props} />
</Wrapper>
);
}
const LeftMenu = ({ version, plugins }) => (
<Wrapper>
<LeftMenuHeader />
<LeftMenuLinkContainer plugins={plugins} />
<LeftMenuFooter key="footer" version={version} />
</Wrapper>
);
export default withRouter(LeftMenu);
LeftMenu.propTypes = {
version: PropTypes.string.isRequired,
plugins: PropTypes.object.isRequired,
};
export default LeftMenu;

View File

@ -77,6 +77,7 @@
"app.components.LeftMenuLinkContainer.installNewPlugin": "Marketplace",
"app.components.LeftMenuLinkContainer.listPlugins": "Plugins",
"app.components.LeftMenuLinkContainer.contentTypes": "Collection Types",
"app.components.LeftMenuLinkContainer.singleTypes": "Single Types",
"app.components.LeftMenuLinkContainer.noPluginsInstalled": "No plugins installed yet",
"app.components.LeftMenuLinkContainer.plugins": "Plugins",
"app.components.LeftMenuLinkContainer.settings": "Settings",

View File

@ -21,11 +21,18 @@ const Initializer = ({ updatePlugin }) => {
try {
const { data } = await request(requestURL, { method: 'GET' });
const menu = [
{
name: 'Content Types',
links: data,
links: data.filter(
contentType => contentType.schema.kind === 'collectionType'
),
},
{
name: 'Single Types',
links: data.filter(
contentType => contentType.schema.kind === 'singleType'
),
},
];