mirror of
https://github.com/strapi/strapi.git
synced 2025-08-11 18:27:22 +00:00
Reskin and refacto the left admin menu and add Single type section
Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
parent
de9521ce6b
commit
4f5cd5a209
@ -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,
|
||||
|
@ -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;
|
||||
|
39
packages/strapi-admin/admin/src/components/LeftMenuLink/A.js
Normal file
39
packages/strapi-admin/admin/src/components/LeftMenuLink/A.js
Normal 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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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;
|
@ -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: '',
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user