This commit is contained in:
Ky 2019-03-07 14:30:09 +01:00
commit 359fdede29
25 changed files with 597 additions and 18 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
import routerPropTypes from './router';
export {
routerPropTypes,
};

View File

@ -0,0 +1,13 @@
import PropTypes from 'prop-types';
const propTypes = (params) => ({
match: PropTypes.shape({
isExact: PropTypes.bool,
params: PropTypes.shape(params),
path: PropTypes.string,
url: PropTypes.string,
}).isRequired,
});
export default propTypes;

View File

@ -0,0 +1,29 @@
/**
*
* LeftMenu
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './styles.scss';
function LeftMenu({ children }) {
return (
<div className={cn(styles.pluginLeftMenu, 'col-md-3')}>
{children}
</div>
);
}
LeftMenu.defaultProps = {
children: null,
};
LeftMenu.propTypes = {
children: PropTypes.node,
};
export default LeftMenu;

View File

@ -0,0 +1,49 @@
.pluginLeftMenu { /* stylelint-disable */
min-height: calc(100vh - 6rem); // TODO should be a global variable
border-radius: 1px;
background-color: #F2F3F4;
box-shadow: inset 0 0 0.2rem 0 rgba(0,0,0,0.05);
padding-top: .4rem;
}
.pluginLeftMenuSection { /* stylelint-disable */
margin-top: 3.3rem;
> p {
-webkit-font-smoothing: antialiased;
margin: 0;
padding-left: 1.5rem;
line-height: 1.3rem;
color: #919BAE;
letter-spacing: 0.1rem;
font-family: Lato;
font-size: 1.1rem;
font-weight: bold;
text-transform: uppercase;
}
> ul {
margin: 1rem 0 0 0rem;
padding: 1rem 0 0 3rem;
font-size: 1.3rem;
color: #2D3138;
> li {
margin-right: .5rem;
margin-bottom: 1.8rem;
line-height: 1.8rem;
overflow-wrap: break-word;
> a {
text-decoration: none;
color: #1C8FE5;
}
}
}
}
.pluginLeftMenuLink { /* stylelint-disable */
color: #2D3138;
li:not(:first-child) {
margin-top: 0.2rem;
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import LeftMenu from '../index';
describe('<LeftMenu />', () => {
it('should not crash', () => {
shallow(<LeftMenu />);
});
it('should render a child if given', () => {
const Child = () => <div>I'm a child</div>;
const wrapper = shallow(<LeftMenu><Child /></LeftMenu>);
expect(wrapper.find(Child).exists()).toBe(true);
});
});

View File

@ -0,0 +1,49 @@
/**
*
* LeftMenuLink
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import { startCase } from 'lodash';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
function LeftMenuLink({ icon, name, source, to }) {
return (
<li className={styles.leftMenuLink}>
<NavLink className={styles.link} to={to} activeClassName={styles.linkActive}>
<div>
<i className={`fa ${icon}`} />
</div>
<div className={styles.container}>
<span className={styles.linkSpan}>{startCase(name)}</span>
{!!source && (
<FormattedMessage id="content-type-builder.from">
{msg => <span id="from-wrapper" style={{ marginLeft: '1rem', fontStyle: 'italic', marginRight: '10px' }}>({msg}: {source})</span>}
</FormattedMessage>
)}
</div>
</NavLink>
</li>
);
}
LeftMenuLink.defaultProps = {
icon: null,
name: null,
source: null,
to: '',
};
LeftMenuLink.propTypes = {
icon: PropTypes.string,
name: PropTypes.string,
source: PropTypes.string,
to: PropTypes.string,
};
export default LeftMenuLink;

View File

@ -0,0 +1,83 @@
.leftMenuLink {
color: #2D3138;
}
li:not(:first-child) {
margin-top: 0.2rem;
}
.link {
display: flex;
margin-left: 2rem;
margin-right: .5rem;
padding-top: 1rem;
padding-left: 1rem;
min-height: 3.4rem;
color: #2C3138 !important;
text-decoration: none !important;
line-height: 1.6rem;
> div:first-child {
width: 1.3rem;
margin-right: 1rem;
font-size: 11px;
> i {
color: #666B74;
}
}
> span {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.linkActive {
height: 3.4rem;
border-radius: 0.2rem;
background-color: #E9EAEB;;
font-weight: bold;
text-decoration: none;
color: #2D3138 !important;
> div {
> i {
color: #2D3138 !important
}
}
}
.liInnerContainer {
display: flex;
margin-left: 2rem;
margin-right: .5rem;
padding-top: 1rem;
padding-left: 1rem;
min-height: 3.4rem;
color: #1C8FE5 !important;
text-decoration: none !important;
line-height: 1.6rem;
cursor: pointer;
> div {
width: 1.3rem;
margin-right: 1rem;
}
}
.linkSpan {
display: block;
width: calc(100% - 95px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.container {
display: flex;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from 'react-intl';
import LeftMenuLink from '../index';
describe('<LeftMenuLink />', () => {
it('Should not crash', () => {
shallow(<LeftMenuLink />);
});
it('should add a span containing from:<source /> if a source prop is given', () => {
const renderedComponent = shallow(<LeftMenuLink to="" name="test" source="source" />);
const sourceInfo = renderedComponent.find(FormattedMessage);
expect(sourceInfo.exists()).toBe(true);
const insideCompo = shallow(sourceInfo.prop('children')());
expect(insideCompo.find('span').length).toBe(1);
});
});

View File

@ -0,0 +1,28 @@
/**
*
* LeftMenuSection
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.scss';
function LeftMenuSection({ children }) {
return (
<div className={styles.leftMenuSection}>
{children}
</div>
);
}
LeftMenuSection.defaultProps = {
children: null,
};
LeftMenuSection.propTypes = {
children: PropTypes.node,
};
export default LeftMenuSection;

View File

@ -0,0 +1,10 @@
.leftMenuSection {
margin-top: 3.3rem;
> ul {
margin: 1rem 0 0 -1.5rem;
padding: 0;
// list-style: none;
font-size: 1.3rem;
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import LeftMenuSection from '../index';
describe('<LeftMenuSection />', () => {
it('should not crash', () => {
shallow(<LeftMenuSection />);
});
it('should render a child if given', () => {
const Child = () => <div>I'm a child</div>;
const wrapper = shallow(<LeftMenuSection><Child /></LeftMenuSection>);
expect(wrapper.find(Child).exists()).toBe(true);
});
});

View File

@ -0,0 +1,29 @@
/**
*
* LeftMenuSectionTitle
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
function LeftMenuSectionTitle({ id }) {
return (
<p className={styles.leftMenuSectionTitle}>
<FormattedMessage id={id} />
</p>
);
}
LeftMenuSectionTitle.defaultProps = {
id: 'app.utils.defaultMessage',
};
LeftMenuSectionTitle.propTypes = {
id: PropTypes.string,
};
export default LeftMenuSectionTitle;

View File

@ -0,0 +1,12 @@
.leftMenuSectionTitle {
-webkit-font-smoothing: antialiased;
margin: 0;
padding-left: 1.5rem;
line-height: 1.3rem;
color: #919BAE;
letter-spacing: 0.1rem;
font-family: Lato;
font-size: 1.1rem;
font-weight: bold;
text-transform: uppercase;
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { shallow } from 'enzyme';
import LeftMenuSectionTitle from '../index';
describe('<LeftMenuSectionTitle />', () => {
it('should not crash', () => {
shallow(<LeftMenuSectionTitle />);
});
});

View File

@ -47,7 +47,7 @@ export class App extends React.Component { // eslint-disable-line react/prefer-s
<Route
key={to}
path={to}
render={props => <Component {...props} {...this.props} />}
render={props => <Component {...this.props} {...props} />}
exact
/>
);

View File

@ -0,0 +1,24 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import cn from 'classnames';
import pluginId from '../../pluginId';
import styles from './styles.scss';
const CustomLink = ({ onClick }) => (
<li style={{ color: '#2D3138' }}>
<div className={cn(styles.linkContainer, styles.iconPlus)} onClick={onClick}>
<div>
<i className="fa fa-plus" />
</div>
<span><FormattedMessage id={`${pluginId}.button.contentType.add`} /></span>
</div>
</li>
);
CustomLink.propTypes = {
onClick: PropTypes.func.isRequired,
};
export default CustomLink;

View File

@ -0,0 +1,17 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
const DocumentationSection = () => (
<ul style={{ marginTop: '20px' }}>
<li style={{ marginLeft: '4.5rem'}}>
<FormattedMessage id="content-type-builder.menu.section.documentation.guide" />&nbsp;
<FormattedMessage id="content-type-builder.menu.section.documentation.guideLink">
{(message) => (
<a href="http://strapi.io/documentation/3.x.x/guides/models.html" target="_blank">{message}</a>
)}
</FormattedMessage>
</li>
</ul>
);
export default DocumentationSection;

View File

@ -5,28 +5,113 @@
*/
import React from 'react';
// import PropTypes from 'prop-types';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { bindActionCreators, compose } from 'redux';
import { get } from 'lodash';
import PluginHeader from 'components/PluginHeader';
import { routerPropTypes } from 'commonPropTypes';
import pluginId from '../../pluginId';
import PluginLeftMenu from '../../components/PluginLeftMenu';
import LeftMenu from '../../components/LeftMenu';
import LeftMenuSection from '../../components/LeftMenuSection';
import LeftMenuSectionTitle from '../../components/LeftMenuSectionTitle';
import LeftMenuLink from '../../components/LeftMenuLink';
import CustomLink from './CustomLink';
import makeSelectModelPage from './selectors';
import reducer from './reducer';
import saga from './saga';
import styles from './styles.scss';
import DocumentationSection from './DocumentationSection';
export class ModelPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
getModelDescription = () => {
const { initialData } = this.props;
const description = get(
initialData,
[this.getModelName(), 'description'],
null,
);
// eslint-disable-next-line no-extra-boolean-cast
return !!description ? description : { id: `${pluginId}.modelPage.contentHeader.emptyDescription.description` };
}
getModelName = () => {
const { match: { params: { modelName } } } = this.props;
return modelName.split('&')[0];
}
getModelNumber = () => {
const { models } = this.props;
return models.length;
}
getSectionTitle = () => {
const base = `${pluginId}.menu.section.contentTypeBuilder.name.`;
return this.getModelNumber() > 1 ? `${base}plural` : `${base}singular`;
}
handleClick = () => {}
renderLinks = () => {
const { models } = this.props;
const links = models.map(model => {
const { name, source } = model;
const base = `/plugins/${pluginId}/models/${name}`;
const to = source ? `${base}&source=${source}` : base;
return (
<LeftMenuLink
key={name}
icon="fa fa-caret-square-o-right"
name={name}
source={source}
to={to}
/>
);
});
return links;
}
render() {
return (
<div className={styles.modelpage}>
<div className="container-fluid">
<div className="row">
<PluginLeftMenu
sections={[]}
/>
<LeftMenu>
<LeftMenuSection>
<LeftMenuSectionTitle id={this.getSectionTitle()} />
<ul>
{this.renderLinks()}
<CustomLink onClick={this.handleClick} />
</ul>
</LeftMenuSection>
<LeftMenuSection>
<LeftMenuSectionTitle id={`${pluginId}.menu.section.documentation.name`} />
<DocumentationSection />
</LeftMenuSection>
</LeftMenu>
<div className="col-md-9">
<div className={styles.componentsContainer}>
<PluginHeader
description={this.getModelDescription()}
title={this.getModelName()}
/>
</div>
</div>
</div>
</div>
</div>
@ -34,13 +119,19 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
}
}
ModelPage.propTypes = {};
ModelPage.propTypes = {
...routerPropTypes(
{ params: PropTypes.string },
).isRequired,
initialData: PropTypes.object.isRequired,
models: PropTypes.array.isRequired,
};
const mapStateToProps = createStructuredSelector({
modelpage: makeSelectModelPage(),
});
function mapDispatchToProps(dispatch) {
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{},
dispatch,

View File

@ -4,7 +4,7 @@
}
.componentsContainer {
padding: 0 1.5rem 0 1.5rem;
padding: 18px 1.5rem 0 1.5rem;
}
.leftMenuSpan {
@ -30,3 +30,21 @@
}
}
}
.linkContainer {
display: flex;
margin-left: 2rem;
margin-right: .5rem;
padding-top: 1rem;
padding-left: 1rem;
min-height: 3.4rem;
color: #1C8FE5 !important;
text-decoration: none !important;
line-height: 1.6rem;
cursor: pointer;
> div {
width: 1.3rem;
margin-right: 1rem;
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { shallow } from 'enzyme';
import CustomLink from '../CustomLink';
describe('<CustomLink />', () => {
it('should not crash', () => {
const onClick = jest.fn();
shallow(<CustomLink onClick={onClick} />);
});
it('should call the onClick prop if needed', () => {
const onClick = jest.fn();
const wrapper = shallow(<CustomLink onClick={onClick} />);
const div = wrapper.find('div').first();
div.simulate('click');
expect(onClick).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,18 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { shallow } from 'enzyme';
import DocumentationSection from '../DocumentationSection';
describe('<DocumentationSection />', () => {
it('should not crash', () => {
shallow(<DocumentationSection />);
});
it('should render a link', () => {
const wrapper = shallow(<DocumentationSection />);
const insideCompo = shallow(wrapper.find(FormattedMessage).at(1).prop('children')());
expect(insideCompo.find('a')).toHaveLength(1);
});
});

View File

@ -5,6 +5,6 @@
describe('<ModelPage />', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
expect(true).toEqual(true);
});
});

View File

@ -10,6 +10,6 @@
describe('defaultSaga Saga', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
expect(true).toEqual(true);
});
});

View File

@ -1,10 +1,27 @@
// import { fromJS } from 'immutable';
// import { makeSelectModelPageDomain } from '../selectors';
import { fromJS } from 'immutable';
import makeSelectModelPageDomain, { selectModelPageDomain } from '../selectors';
import pluginId from '../../../pluginId';
// const selector = makeSelectModelPageDomain();
describe('makeSelectModelPageDomain', () => {
it('Expect to have unit tests specified', () => {
expect(true).toEqual(false);
describe('<ModelPage />, selectors', () => {
describe('makeSelectModelPageDomain', () => {
it('should return the globalState (.toJS())', () => {
const mockedState = fromJS({
[`${pluginId}_modelPage`]: fromJS({}),
});
expect(makeSelectModelPageDomain()(mockedState)).toEqual({});
});
});
describe('selectModelPageDomain', () => {
it('should return the globalState', () => {
const mockedState = fromJS({
[`${pluginId}_modelPage`]: fromJS({}),
});
expect(selectModelPageDomain()(mockedState)).toEqual(fromJS({}));
});
});
});