Attributes list & delete attribute in GroupPage

This commit is contained in:
Virginie Ky 2019-07-04 12:00:35 +02:00
parent d576b2129a
commit c37f9e54df
13 changed files with 458 additions and 31 deletions

View File

@ -42,6 +42,18 @@ const List = styled.div`
}
}
}
td {
padding: 0.75em;
vertical-align: middle;
font-size: 1.3rem;
line-height: 1.8rem;
&:first-of-type {
padding-left: calc(3rem + 0.75em);
}
&:last-of-type {
padding-right: calc(3rem + 0.75em);
}
}
tbody {
color: ${colors.blueTxt};
tr {
@ -65,18 +77,6 @@ const List = styled.div`
height: 0;
}
}
td {
padding: 0.75em;
vertical-align: middle;
font-size: 1.3rem;
line-height: 1.8rem;
&:first-of-type {
padding-left: calc(3rem + 0.75em);
}
&:last-of-type {
padding-right: calc(3rem + 0.75em);
}
}
}
@media (min-width: ${sizes.tablet}) {
width: 100%;

View File

@ -12,6 +12,12 @@ const ListWrapper = styled.div`
width: 100%;
overflow-x: scroll;
}
.list-button {
padding: 10px 30px 25px 30px;
button {
width: 100%;
}
}
@media (min-width: ${sizes.tablet}) {
.table-wrapper {
width: 100%;

View File

@ -0,0 +1,39 @@
/**
*
* StyedListRow
*
*/
import styled from 'styled-components';
const StyedListRow = styled.tr`
background-color: transparent;
cursor: pointer;
p {
margin-bottom: 0;
}
img {
width: 35px;
}
&:hover {
background-color: #f7f8f8;
}
td:first-of-type {
padding-left: 3rem;
}
td:nth-child(2) {
width: 25rem;
p {
font-weight: 500;
text-transform: capitalize;
}
}
td:last-child {
text-align: right;
}
button {
cursor: pointer;
}
`;
export default StyedListRow;

View File

@ -0,0 +1,27 @@
import boolean from '../../assets/images/icon_boolean.png';
import date from '../../assets/images/icon_date.png';
import email from '../../assets/images/icon_email.png';
import enumeration from '../../assets/images/icon_enumeration.png';
import media from '../../assets/images/icon_media.png';
import json from '../../assets/images/icon_json.png';
import number from '../../assets/images/icon_number.png';
import password from '../../assets/images/icon_password.png';
import relation from '../../assets/images/icon_relation.png';
import string from '../../assets/images/icon_string.png';
import text from '../../assets/images/icon_text.png';
const assets = {
boolean,
date,
email,
enumeration,
media,
json,
number,
password,
relation,
string,
text,
};
export default assets;

View File

@ -0,0 +1,146 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { PopUpWarning } from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import StyledListRow from './StyledListRow';
import assets from './assets';
function ListRow({
canOpenModal,
deleteAttribute,
isTemporary,
name,
onClickGoTo,
source,
uid,
target,
type,
}) {
const [isOpen, setIsOpen] = useState(false);
const ico = ['integer', 'biginteger', 'float', 'decimal'].includes(type)
? 'number'
: type;
const src = target ? assets.relation : assets[ico];
return (
<>
<StyledListRow
onClick={e => {
e.stopPropagation();
const to = uid || name;
onClickGoTo(to, source);
}}
>
<td>
<img src={src} alt={`icon-${ico}`} />
</td>
<td>
<p>
{name}
{source && (
<FormattedMessage id={`${pluginId}.from`}>
{message => (
<span
style={{
fontStyle: 'italic',
color: '#787E8F',
fontWeight: '500',
}}
>
&nbsp;({message}: {source})
</span>
)}
</FormattedMessage>
)}
&nbsp; &nbsp; &nbsp;
{isTemporary && (
<FormattedMessage
id={`${pluginId}.contentType.temporaryDisplay`}
/>
)}
</p>
</td>
<td>
<FormattedMessage id={`${pluginId}.attribute.${type}`} />
</td>
<td>
{!source && (
<>
<button
type="button"
onClick={e => {
e.stopPropagation();
const to = uid || name;
onClickGoTo(to, source, canOpenModal || isTemporary);
}}
>
<i className="fa fa-pencil link-icon" />
</button>
<button
type="button"
onClick={e => {
e.stopPropagation();
if (canOpenModal || isTemporary) {
setIsOpen(true);
} else {
strapi.notification.info(
`${pluginId}.notification.info.work.notSaved`
);
}
}}
>
<i className="fa fa-trash link-icon" />
</button>
<PopUpWarning
isOpen={isOpen}
toggleModal={() => setIsOpen(prevState => !prevState)}
content={{
message: `${pluginId}.popUpWarning.bodyMessage.${
type === 'models' ? 'contentType' : 'groups'
}.delete`,
}}
type="danger"
onConfirm={() => {
setIsOpen(false);
deleteAttribute(name);
}}
/>
</>
)}
</td>
</StyledListRow>
</>
);
}
ListRow.defaultProps = {
target: null,
source: null,
uid: null,
deleteAttribute: () => {},
};
ListRow.propTypes = {
canOpenModal: PropTypes.bool,
context: PropTypes.object,
deleteAttribute: PropTypes.func,
isTemporary: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
onClickGoTo: PropTypes.func.isRequired,
source: PropTypes.string,
uid: PropTypes.string,
type: PropTypes.string.isRequired,
};
export default ListRow;

View File

@ -15,6 +15,7 @@ import {
CREATE_TEMP_CONTENT_TYPE,
CREATE_TEMP_GROUP,
DELETE_GROUP,
DELETE_GROUP_ATTRIBUTE,
DELETE_GROUP_SUCCEEDED,
DELETE_MODEL,
DELETE_MODEL_ATTRIBUTE,
@ -113,6 +114,13 @@ export function deleteGroup(uid) {
};
}
export function deleteGroupAttribute(keys) {
return {
type: DELETE_GROUP_ATTRIBUTE,
keys,
};
}
export function deleteGroupSucceeded(uid) {
return {
type: DELETE_GROUP_SUCCEEDED,

View File

@ -20,6 +20,8 @@ export const CREATE_TEMP_CONTENT_TYPE =
'ContentTypeBuilder/App/CREATE_TEMP_CONTENT_TYPE';
export const CREATE_TEMP_GROUP = 'ContentTypeBuilder/App/CREATE_TEMP_GROUP';
export const DELETE_GROUP = 'ContentTypeBuilder/App/DELETE_GROUP';
export const DELETE_GROUP_ATTRIBUTE =
'ContentTypeBuilder/App/DELETE_GROUP_ATTRIBUTE';
export const DELETE_GROUP_SUCCEEDED =
'ContentTypeBuilder/App/DELETE_GROUP_SUCCEEDED';
export const DELETE_MODEL = 'ContentTypeBuilder/App/DELETE_MODEL';

View File

@ -15,6 +15,7 @@ import {
CLEAR_TEMPORARY_ATTRIBUTE_RELATION,
CREATE_TEMP_CONTENT_TYPE,
CREATE_TEMP_GROUP,
DELETE_GROUP_ATTRIBUTE,
DELETE_GROUP_SUCCEEDED,
DELETE_MODEL_ATTRIBUTE,
DELETE_MODEL_SUCCEEDED,
@ -258,6 +259,20 @@ function appReducer(state = initialState, action) {
)
)
.update('newGroupClone', () => state.get('newGroup'));
case DELETE_GROUP_ATTRIBUTE: {
const pathToAttributes = action.keys
.slice()
.reverse()
.splice(1)
.reverse();
const attributes = state.getIn(pathToAttributes);
const attributeName = action.keys.pop();
const attributeToDelete = attributes.findIndex(
attribute => attribute.get('name') === attributeName
);
return state.removeIn([...pathToAttributes, attributeToDelete]);
}
case DELETE_GROUP_SUCCEEDED:
console.log({
st: state

View File

@ -1,22 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { get, pickBy } from 'lodash';
import pluginId from '../../pluginId';
import ViewContainer from '../ViewContainer';
import AttributesModalPicker from '../AttributesPickerModal';
import ListRow from '../../components/ListRow';
import {
BackHeader,
Button,
EmptyAttributesBlock,
getQueryParameters,
ListWrapper,
ListHeader,
List,
} from 'strapi-helper-plugin';
import { deleteGroupAttribute } from '../App/actions';
/* eslint-disable no-extra-boolean-cast */
class GroupPage extends React.Component {
export class GroupPage extends React.Component {
featureType = 'group';
getFeature = () => {
const { modifiedDataGroup, newGroup } = this.props;
if (this.isUpdatingTempFeature()) {
return newGroup;
}
return get(modifiedDataGroup, this.getFeatureName(), {});
};
getFeatureSchema = () => get(this.getFeature(), 'schema', {});
getFeatureAttributes = () => get(this.getFeatureSchema(), 'attributes', []);
getFeatureAttributesLength = () =>
Object.keys(this.getFeatureAttributes()).length;
getFeatureName = () => {
const {
match: {
@ -66,6 +93,21 @@ class GroupPage extends React.Component {
return search;
};
handleDeleteGroupAttribute = attrToDelete => {
const { deleteGroupAttribute } = this.props;
const keys = this.isUpdatingTempFeature()
? ['newGroup', 'schema', 'attributes', attrToDelete]
: [
'modifiedDataGroup',
this.getFeatureName(),
'schema',
'attributes',
attrToDelete,
];
deleteGroupAttribute(keys);
};
handleGoBack = () => this.props.history.goBack();
isUpdatingTempFeature = () => {
@ -77,9 +119,22 @@ class GroupPage extends React.Component {
render() {
const {
canOpenModal,
history: { push },
} = this.props;
const attributes = this.getFeatureAttributes();
const attributesNumber = this.getFeatureAttributesLength();
let listTitle = `${pluginId}.table.attributes.title.${
attributesNumber > 1 ? 'plural' : 'singular'
}`;
const buttonProps = {
kind: 'secondaryHotlineAdd',
label: `${pluginId}.button.attributes.add`,
onClick: this.handleClick,
};
return (
<>
<BackHeader onClick={this.handleGoBack} />
@ -89,14 +144,47 @@ class GroupPage extends React.Component {
headerTitle={this.getFeatureHeaderTitle()}
headerDescription={this.getFeatureHeaderDescription()}
>
<EmptyAttributesBlock
description={`${pluginId}.home.emptyAttributes.description.${
this.featureType
}`}
id="openAddAttr"
label="content-type-builder.button.attributes.add"
title="content-type-builder.home.emptyAttributes.title"
/>
{attributesNumber === 0 ? (
<EmptyAttributesBlock
description={`${pluginId}.home.emptyAttributes.description.${
this.featureType
}`}
id="openAddAttr"
label="content-type-builder.button.attributes.add"
title="content-type-builder.home.emptyAttributes.title"
/>
) : (
<ListWrapper>
<ListHeader
title={listTitle}
titleValues={{ number: attributesNumber }}
relationTitle={listTitle}
relationTitleValues={{ number: attributesNumber }}
button={{ ...buttonProps }}
/>
<List>
<table>
<tbody>
{attributes.map(attribute => (
<ListRow
key={attribute.name}
canOpenModal={canOpenModal}
context={this.context}
deleteAttribute={this.handleDeleteGroupAttribute}
{...attribute}
type={attribute.type}
isTemporary={false}
onClickGoTo={() => {}}
/>
))}
</tbody>
</table>
</List>
<div className="list-button">
<Button {...buttonProps} />
</div>
</ListWrapper>
)}
</ViewContainer>
<AttributesModalPicker
isOpen={this.getModalType() === 'chooseAttributes'}
@ -108,6 +196,7 @@ class GroupPage extends React.Component {
}
GroupPage.propTypes = {
deleteGroupAttribute: PropTypes.func.isRequired,
groups: PropTypes.array.isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
@ -124,4 +213,18 @@ GroupPage.propTypes = {
newGroup: PropTypes.object.isRequired,
};
export default GroupPage;
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
deleteGroupAttribute,
},
dispatch
);
}
const withConnect = connect(
null,
mapDispatchToProps
);
export default compose(withConnect)(GroupPage);

View File

@ -1,10 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import GroupPage from '../index';
import { EmptyAttributesBlock } from 'strapi-helper-plugin';
import { GroupPage, mapDispatchToProps } from '../index';
import { deleteGroupAttribute } from '../../App/actions';
const basePath = '/plugins/content-type-builder/groups';
const props = {
deleteGroupAttribute: jest.fn(),
groups: [
{
icon: 'fa-cube',
@ -89,6 +93,17 @@ describe('CTB <GroupPage />', () => {
shallow(<GroupPage {...props} />);
});
describe('CTB <ModelPage /> render', () => {
it("should display the EmptyAttributeBlock if the group's attributes are empty", () => {
props.initialDataGroup.tests.schema.attributes = {};
props.modifiedDataGroup.tests.schema.attributes = {};
const wrapper = shallow(<GroupPage {...props} />);
expect(wrapper.find(EmptyAttributesBlock)).toHaveLength(1);
});
});
describe('GetFeatureHeaderDescription', () => {
it("should return the model's description field", () => {
const { getFeatureHeaderDescription } = shallow(
@ -99,7 +114,22 @@ describe('CTB <GroupPage />', () => {
});
});
describe('getFeatureName', () => {
describe('GetFeature', () => {
it('should return the correct model', () => {
const { getFeature } = shallow(<GroupPage {...props} />).instance();
expect(getFeature()).toEqual(props.modifiedDataGroup.tests);
});
it('should return newGroup isTemporary is true', () => {
props.groups.find(item => item.name == 'tests').isTemporary = true;
const { getFeature } = shallow(<GroupPage {...props} />).instance();
expect(getFeature()).toEqual(props.newGroup);
});
});
describe('GetFeatureName', () => {
it("should return the model's name field", () => {
const { getFeatureName } = shallow(<GroupPage {...props} />).instance();
@ -107,3 +137,37 @@ describe('CTB <GroupPage />', () => {
});
});
});
describe('CTB <GroupPage />, mapDispatchToProps', () => {
describe('DeleteGroupAttribute', () => {
it('should be injected', () => {
const dispatch = jest.fn();
const result = mapDispatchToProps(dispatch);
expect(result.deleteGroupAttribute).toBeDefined();
});
});
it('should call deleteGroupAttribute with modifiedDataGroup path when isTemporary is false', () => {
props.groups.find(item => item.name == 'tests').isTemporary = false;
const { handleDeleteGroupAttribute } = shallow(
<GroupPage {...props} />
).instance();
handleDeleteGroupAttribute('name');
const keys = ['modifiedDataGroup', 'tests', 'schema', 'attributes', 'name'];
expect(props.deleteGroupAttribute).toHaveBeenCalledWith(keys);
});
it('should call deleteGroupAttribute with modifiedDataGroup path when isTemporary is true', () => {
props.groups.find(item => item.name == 'tests').isTemporary = true;
const { handleDeleteGroupAttribute } = shallow(
<GroupPage {...props} />
).instance();
handleDeleteGroupAttribute('name');
const keys = ['newGroup', 'schema', 'attributes', 'name'];
expect(props.deleteGroupAttribute).toHaveBeenCalledWith(keys);
});
});

View File

@ -191,5 +191,7 @@
"table.contentType.title.singular": "{number} Content Type is available",
"table.groups.title.plural": "{number} Groups are available",
"table.groups.title.singular": "{number} Group is available",
"table.attributes.title.plural": "{number} fields",
"table.attributes.title.singular": "{number} field",
"prompt.content.unsaved": "Are you sure you want to leave this content type? All your modifications will be lost."
}

View File

@ -185,5 +185,7 @@
"table.contentType.title.singular": "{number} Type de Contenu est disponible",
"table.group.title.plural": "{number} Groupes sont disponibles",
"table.group.title.singular": "{number} Groupe est disponible",
"table.attributes.title.plural": "{number} chammps",
"table.attributes.title.singular": "{number} champ",
"prompt.content.unsaved": "Etes-vous sûr de vouloir quitter ce model? Toutes vos modifications seront perdues."
}

View File

@ -25,6 +25,7 @@ module.exports = {
model: 'file',
via: 'related',
plugin: 'upload',
type: 'media',
},
},
},
@ -50,6 +51,23 @@ module.exports = {
model: 'file',
via: 'related',
plugin: 'upload',
type: 'media',
},
},
},
},
{
uid: 'cats',
name: 'Cats',
source: null,
schema: {
connection: 'default',
collectionName: 'cats',
description: 'Little description for cats group',
attributes: {
name: {
type: 'string',
required: true,
},
},
},
@ -62,12 +80,7 @@ module.exports = {
connection: 'default',
collectionName: 'cars',
description: 'Little description for cars group',
attributes: {
name: {
type: 'string',
required: true,
},
},
attributes: {},
},
},
//...