Merge pull request #3897 from strapi/front/qa

Repeatable fields QA
This commit is contained in:
cyril lopez 2019-09-04 16:22:27 +02:00 committed by GitHub
commit 106260da45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 354 additions and 177 deletions

View File

@ -1,5 +1,5 @@
// Import
@import "../../styles/variables/variables";
@import '../../styles/variables/variables';
.item {
position: relative;
@ -12,13 +12,14 @@
.plugin {
cursor: pointer;
position: absolute;
top: 10px; left: calc(100% - 4px);
top: 10px;
left: calc(100% - 4px);
display: inline-block;
width: auto;
height: 20px;
transition: right 1s ease-in-out;
span{
span {
display: inline-block;
overflow: hidden;
width: auto;
@ -29,10 +30,10 @@
line-height: 20px;
background: #0097f7;
border-radius: 3px;
transition: transform .3s ease-in-out;
transition: transform 0.3s ease-in-out;
white-space: pre;
&:hover{
&:hover {
transform: translateX(calc(-100% + 9px));
}
}
@ -40,7 +41,8 @@
}
.link {
padding-top: .9rem;
position: relative;
padding-top: 0.8rem;
padding-bottom: 0.2rem;
padding-left: 1.6rem;
min-height: 3.6rem;
@ -66,6 +68,15 @@
&:visited {
color: $left-menu-link-color;
}
span {
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.linkActive {
@ -74,7 +85,9 @@
}
.linkIcon {
position: relative;
position: absolute;
top: calc(50% - 0.9rem + 0.6rem);
left: 1.6rem;
margin-right: 1.2rem;
font-size: 1.2rem;
width: 1.4rem;
@ -85,5 +98,7 @@
.linkLabel {
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
}

View File

@ -0,0 +1,16 @@
/**
*
* StyledPluginHeader
*
*/
import styled from 'styled-components';
const StyledPluginHeader = styled.div`
margin-bottom: 30px;
.justify-content-end {
display: flex;
}
`;
export default StyledPluginHeader;

View File

@ -6,12 +6,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import PluginHeaderTitle from '../PluginHeaderTitle';
import PluginHeaderActions from '../PluginHeaderActions';
import styles from './styles.scss';
import StyledPluginHeader from './StyledPluginHeader';
function PluginHeader({
actions,
@ -25,8 +24,9 @@ function PluginHeader({
withDescriptionAnim,
}) {
return (
<div className={cn(styles.pluginHeader, 'row')}>
<div className="col-lg-7">
<StyledPluginHeader>
<div className="row">
<div className="col-lg-6">
<PluginHeaderTitle
icon={icon}
onClickIcon={onClickIcon}
@ -36,16 +36,15 @@ function PluginHeader({
withDescriptionAnim={withDescriptionAnim}
/>
</div>
<div className="col-lg-2 justify-content-end">
<div className="col-lg-6 justify-content-end">
<PluginHeaderActions actions={subActions} />
</div>
<div className="col-lg-3 justify-content">
<PluginHeaderActions
actions={actions}
overrideRendering={overrideRendering}
/>
</div>
</div>
</StyledPluginHeader>
);
}

View File

@ -1,3 +0,0 @@
.pluginHeader {
margin-bottom: 30px;
}

View File

@ -0,0 +1,26 @@
/**
*
* StyledPluginHeaderActions
*
*/
import styled from 'styled-components';
const StyledPluginHeaderActions = styled.div`
display: flex;
justify-content: flex-end;
width: fit-content;
max-width: 100%;
padding-top: 0.9rem;
button {
margin-right: 0;
margin-left: 1.8rem;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
`;
export default StyledPluginHeaderActions;

View File

@ -10,26 +10,20 @@ import { isArray, isFunction } from 'lodash';
import Button from '../Button';
import styles from './styles.scss';
import StyledPluginHeaderActions from './StyledPluginHeaderActions';
function PluginHeaderActions({ actions, overrideRendering }) {
let content = '';
if (isArray(actions)) {
content = actions.map(action => (
<Button {...action} key={action.label} />
));
content = actions.map(action => <Button {...action} key={action.label} />);
}
if (isFunction(overrideRendering)) {
content = overrideRendering();
}
return (
<div className={styles.pluginHeaderActions}>
{content}
</div>
);
return <StyledPluginHeaderActions>{content}</StyledPluginHeaderActions>;
}
PluginHeaderActions.defaultProps = {
@ -38,14 +32,8 @@ PluginHeaderActions.defaultProps = {
};
PluginHeaderActions.propTypes = {
actions: PropTypes.oneOfType([
PropTypes.array,
PropTypes.bool,
]),
overrideRendering: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func,
]),
actions: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]),
overrideRendering: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
};
export default PluginHeaderActions;

View File

@ -1,6 +0,0 @@
.pluginHeaderActions {
display: flex;
justify-content: flex-end;
margin-top: 9px;
margin-right: -18px;
}

View File

@ -0,0 +1,48 @@
/**
*
* StyledPluginHeaderTitle
*
*/
import styled from 'styled-components';
const StyledPluginHeaderTitle = styled.div`
.header-title {
position: relative;
h1 {
position: relative;
width: fit-content;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 2.4rem;
font-weight: 600;
margin-top: 0.7rem;
margin-bottom: 1px;
text-transform: capitalize;
padding-right: 18px;
}
i {
position: absolute;
right: 0;
top: 0;
margin-top: 9px;
font-size: 14px;
color: rgba(16, 22, 34, 0.35);
cursor: pointer;
}
}
.header-subtitle {
width: 100%;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.3rem;
font-weight: 400;
color: #787e8f;
}
`;
export default StyledPluginHeaderTitle;

View File

@ -11,7 +11,7 @@ import { isEmpty, isFunction, isObject } from 'lodash';
import LoadingBar from '../LoadingBar';
import styles from './styles.scss';
import StyledPluginHeaderTitle from './StyledPluginHeaderTitle';
function PluginHeaderTitle({
description,
@ -25,28 +25,26 @@ function PluginHeaderTitle({
const contentDescription = formatData(description);
return (
<div>
<div style={{ display: 'flex' }}>
<h1 className={styles.pluginHeaderTitleName} id={titleId}>
{contentTitle}&nbsp;
</h1>
<StyledPluginHeaderTitle>
<div className="header-title">
<h1 id={titleId}>
{contentTitle}
{icon && (
<i
className={`${icon} ${styles.icon}`}
className={`${icon}`}
id="editCTName"
onClick={onClickIcon}
role="button"
/>
)}
</h1>
</div>
{withDescriptionAnim ? (
<LoadingBar style={{ marginTop: '13px' }} />
) : (
<p className={styles.pluginHeaderTitleDescription}>
{contentDescription}&nbsp;
</p>
<p className="header-subtitle">{contentDescription}&nbsp;</p>
)}
</div>
</StyledPluginHeaderTitle>
);
}

View File

@ -1,23 +0,0 @@
.pluginHeaderTitleName {
font-size: 2.4rem;
font-weight: 600;
margin-top: 0.7rem;
margin-bottom: 1px;
text-transform: capitalize;
}
.pluginHeaderTitleDescription {
font-size: 1.3rem;
font-weight: 400;
color: #787E8F;
margin: 0;
}
.icon {
margin-top: 16px;
margin-left: 3px;
color: rgba(16,22,34,.35);
&:hover {
cursor: pointer;
}
}

View File

@ -43,4 +43,5 @@ const TrashButton = styled.div`
}
}
`;
export default TrashButton;

View File

@ -106,11 +106,7 @@ const Icon = styled.i`
}}
`;
const Truncate = styled.div`
// display: table;
// table-layout: fixed;
// width: 100%;
`;
const Truncate = styled.div``;
const Truncated = styled.p`
overflow-x: hidden;

View File

@ -80,7 +80,9 @@ const Wrapper = styled(Flex)`
} else {
return '#ffffff';
}
}}
}};
${({ hasErrors, isOpen }) => {
if (hasErrors) {
return css`
@ -88,7 +90,6 @@ const Wrapper = styled(Flex)`
font-weight: 600;
`;
}
if (isOpen) {
return css`
color: #007eff;
@ -96,13 +97,24 @@ const Wrapper = styled(Flex)`
`;
}
}}
button,
i, img {
&:active,
&:focus {
outline: 0;
}
${({ isOpen }) => {
if (isOpen) {
return css`
&.trash-icon i {
color: #007eff;
}
`;
}
}}
webkit-font-smoothing: antialiased;
`;

View File

@ -5,7 +5,7 @@ const ListWrapper = styled.div`
max-height: 116px;
> ul {
margin: 4px -20px 0;
margin: 0 -20px 0;
padding: 0 20px !important;
list-style: none !important;
overflow: auto;
@ -37,14 +37,13 @@ const Li = styled.li`
flex-wrap: nowrap;
align-content: center;
justify-content: space-between;
height: 27px;
background-color: transparent !important;
margin-bottom: 9px;
height: 18px;
margin-top: 9px;
&:last-of-type {
margin-bottom: 0px;
}
&:active {
.dragHandle {
// cursor: pointer;
> span {
background: #aed4fb;
}
@ -132,7 +131,7 @@ const Li = styled.li`
display: inline-block;
height: 100%;
padding-right: 0px;
line-height: 27px;
line-height: 18px;
text-align: right;
white-space: nowrap;
overflow: hidden;

View File

@ -5,6 +5,10 @@ const Wrapper = styled.div`
margin-bottom: 27px;
label {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.3rem;
font-weight: 500;
}
@ -94,11 +98,6 @@ const Wrapper = styled.div`
}
}
}
& + div {
ul {
margin-bottom: -4px;
}
}
}
`;

View File

@ -380,9 +380,7 @@ function EditView({
label: `${pluginId}.containers.Edit.submit`,
type: 'submit',
loader: isSubmitting,
style: isSubmitting
? { marginRight: '18px', flexGrow: 2 }
: { flexGrow: 2 },
style: isSubmitting ? { marginRight: '18px' } : {},
disabled: isSubmitting, // TODO STATE WHEN SUBMITING
},
]}

View File

@ -0,0 +1,32 @@
/**
*
* StyledBackButton
*
*/
import styled from 'styled-components';
const StyledBackButton = styled.div`
position: fixed;
top: 0;
height: 6rem;
width: 6.5rem;
line-height: 6rem;
z-index: 1050;
text-align: center;
background-color: #ffffff;
color: #81848a;
border-top: 1px solid #f3f4f4;
border-right: 1px solid #f3f4f4;
border-left: 1px solid #f3f4f4;
cursor: pointer;
i {
font-size: 1.8rem;
font-weight: bolder;
}
&:hover {
background-color: #f3f4f4;
}
`;
export default StyledBackButton;

View File

@ -0,0 +1,24 @@
/**
*
* BackButton
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import StyledBackButton from './StyledBackButton';
function BackButton({ onClick }) {
return (
<StyledBackButton onClick={onClick}>
<i className="fa fa-chevron-left"></i>
</StyledBackButton>
);
}
BackButton.propTypes = {
onClick: PropTypes.func.isRequired,
};
export default BackButton;

View File

@ -23,6 +23,8 @@ import GroupPage from '../GroupPage';
import Loader from './Loader';
import BackButton from '../../components/BackButton';
import {
addAttributeRelation,
cancelNewContentType,
@ -77,6 +79,11 @@ const ROUTES = [
export class App extends React.Component {
// eslint-disable-line react/prefer-stateless-function
state = {
routerHistory: [],
historyCount: 1,
};
componentDidMount() {
this.props.getData();
}
@ -86,6 +93,13 @@ export class App extends React.Component {
if (prevProps.shouldRefetchData !== this.props.shouldRefetchData) {
this.props.getData();
}
if (prevProps.location !== this.props.location) {
if (prevProps.location.pathname !== this.getPathname()) {
this.addRouterHistory(prevProps.location.pathname);
} else {
this.increaseHistoryCount();
}
}
}
componentWillUnmount() {
@ -101,8 +115,51 @@ export class App extends React.Component {
);
};
addRouterHistory = pathname => {
if (this.getLastPathname() !== this.getPathname()) {
this.setState(prevState => {
return {
routerHistory: [...prevState.routerHistory, pathname],
historyCount: prevState.historyCount + 1,
};
});
}
};
increaseHistoryCount = () => {
this.setState(prevState => {
return {
routerHistory: [...prevState.routerHistory],
historyCount: prevState.historyCount + 1,
};
});
};
removeRouterHistory = () => {
const array = [...this.state.routerHistory];
const index = array.length - 1;
if (index !== -1) {
array.splice(index, 1);
this.setState(prevState => {
return {
routerHistory: array,
historyCount: prevState.historyCount + 1,
};
});
}
};
getSearch = () => this.props.location.search;
getPathname = () => this.props.location.pathname;
getLastPathname = () => {
const { routerHistory } = this.state;
return routerHistory[routerHistory.length - 1];
};
getActionType = () => {
return getQueryParameters(this.getSearch(), 'actionType');
};
@ -140,6 +197,24 @@ export class App extends React.Component {
getFeatureNameFromSearch = () =>
getQueryParameters(this.getSearch(), `${this.getFeatureType()}Name`);
handleGoBack = async () => {
const { history } = this.props;
const { routerHistory, historyCount } = this.state;
await this.wait();
if (routerHistory.length > 0) {
history.push(this.getLastPathname());
this.removeRouterHistory();
} else {
history.go(-historyCount);
}
};
wait = async () => {
return new Promise(resolve => setTimeout(resolve, 200));
};
isUpdatingTemporaryModel = (modelName = this.getFeatureNameFromSearch()) => {
const { models } = this.props;
@ -194,7 +269,7 @@ export class App extends React.Component {
createTempGroup,
groups,
history: { push },
location: { pathname, search },
location: { search },
isLoading,
models,
onChangeExistingContentTypeMainInfos,
@ -227,7 +302,7 @@ export class App extends React.Component {
modifiedData: this.getFormDataForModel(),
onChangeExistingFeatureMainInfos: onChangeExistingContentTypeMainInfos,
onChangeNewFeatureMainInfos: onChangeNewContentTypeMainInfos,
pathname,
pathname: this.getPathname(),
push,
resetExistingFeatureMainInfos: resetExistingContentTypeMainInfos,
resetNewFeatureMainInfos: resetNewContentTypeMainInfos,
@ -246,7 +321,7 @@ export class App extends React.Component {
modifiedData: this.getFormDataForGroup(),
onChangeExistingFeatureMainInfos: onChangeExistingGroupMainInfos,
onChangeNewFeatureMainInfos: onChangeNewGroupMainInfos,
pathname,
pathname: this.getPathname(),
push,
resetExistingFeatureMainInfos: resetExistingGroupMainInfos,
resetNewFeatureMainInfos: () => {},
@ -264,6 +339,7 @@ export class App extends React.Component {
}}
>
<div className={styles.app}>
<BackButton onClick={this.handleGoBack}></BackButton>
<Switch>
{ROUTES.map(this.renderRoute)}
<Route component={NotFound} />

View File

@ -17,7 +17,6 @@ import ViewContainer from '../ViewContainer';
import RelationFormGroup from '../RelationFormGroup';
import {
BackHeader,
Button,
EmptyAttributesBlock,
getQueryParameters,
@ -316,10 +315,6 @@ export class GroupPage extends React.Component {
this.setState({ attrToDelete: null, showDeleteAttrWarning: false });
};
handleGoBack = () => {
this.props.history.goBack();
};
handleSubmit = (shouldContinue = false) => {
const {
addAttributeRelationGroup,
@ -523,7 +518,6 @@ export class GroupPage extends React.Component {
return (
<>
<BackHeader onClick={this.handleGoBack} />
<FormattedMessage id={`${pluginId}.prompt.content.unsaved`}>
{msg => (
<Prompt
@ -715,7 +709,6 @@ GroupPage.propTypes = {
groups: PropTypes.array.isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
}),
initialDataGroup: PropTypes.object.isRequired,
location: PropTypes.shape({

View File

@ -69,7 +69,6 @@ const props = {
],
history: {
push: jest.fn(),
goBack: jest.fn(),
},
initialDataGroup: {
tests: {
@ -232,15 +231,6 @@ describe('CTB <GroupPage />', () => {
});
});
describe('HandleGoBack', () => {
it('should go to previous page', () => {
const { handleGoBack } = shallow(<GroupPage {...props} />).instance();
handleGoBack();
expect(props.history.goBack).toHaveBeenCalled();
});
});
describe('toggleDeleteAttrModalWarning', () => {
const wrapper = shallow(<GroupPage {...props} />);
expect(wrapper.state()).toEqual({

View File

@ -14,7 +14,6 @@ import { get, isEqual, pickBy } from 'lodash';
import { Prompt } from 'react-router';
import {
BackHeader,
Button,
EmptyAttributesBlock,
List,
@ -381,17 +380,16 @@ export class ModelPage extends React.Component {
this.setState({ attrToDelete: null, showDeleteAttrWarning: false });
};
handleGoBack = () => {
this.props.history.goBack();
};
handleRedirectToGroup = group => {
const {
history: { push },
} = this.props;
const { source, uid } = group;
const base = `/plugins/${pluginId}/groups/${uid}`;
const to = source ? `${base}&source=${source}` : base;
this.props.history.push(to);
push(to);
};
handleSubmit = (shouldContinue = false) => {
@ -599,7 +597,6 @@ export class ModelPage extends React.Component {
return (
<div className={styles.modelpage}>
<BackHeader onClick={this.handleGoBack} />
<FormattedMessage id={`${pluginId}.prompt.content.unsaved`}>
{msg => (
<Prompt

View File

@ -12,11 +12,6 @@ const StyledViewContainer = styled.div`
min-height: calc(100vh - ${sizes.header.height});
.components-container {
padding: 1.8rem 1.5rem;
div div:not(.list-button) {
button {
top: 1.8rem;
}
}
> div:not(:first-of-type):not(:last-of-type) {
> div:first-of-type {
padding-bottom: 1rem;
@ -35,9 +30,16 @@ const StyledViewContainer = styled.div`
.trash-btn-wrapper {
position: relative;
width: 100%;
padding-top: 3.4rem;
padding-top: 3.1rem;
display: flex;
justify-content: flex-end;
> div {
height: 30px;
line-height: 30px;
> div {
padding: 0 15px;
}
}
}
}
`;