diff --git a/packages/strapi-helper-plugin/lib/src/components/List/index.js b/packages/strapi-helper-plugin/lib/src/components/List/index.js index d3ee557675..90909541a1 100644 --- a/packages/strapi-helper-plugin/lib/src/components/List/index.js +++ b/packages/strapi-helper-plugin/lib/src/components/List/index.js @@ -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%; diff --git a/packages/strapi-helper-plugin/lib/src/components/ListWrapper/index.js b/packages/strapi-helper-plugin/lib/src/components/ListWrapper/index.js index 6c9f99268c..0d78297ae9 100644 --- a/packages/strapi-helper-plugin/lib/src/components/ListWrapper/index.js +++ b/packages/strapi-helper-plugin/lib/src/components/ListWrapper/index.js @@ -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%; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/StyledListRow.js b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/StyledListRow.js new file mode 100644 index 0000000000..ab1cb499d9 --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/StyledListRow.js @@ -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; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/assets.js b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/assets.js new file mode 100644 index 0000000000..60db15038e --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/assets.js @@ -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; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/index.js b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/index.js new file mode 100644 index 0000000000..c354371979 --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/admin/src/components/ListRow/index.js @@ -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 ( + <> + { + e.stopPropagation(); + + const to = uid || name; + onClickGoTo(to, source); + }} + > + + + + + + {name} + {source && ( + + {message => ( + + ({message}: {source}) + + )} + + )} + + {isTemporary && ( + + )} + + + + + + + {!source && ( + <> + { + e.stopPropagation(); + + const to = uid || name; + onClickGoTo(to, source, canOpenModal || isTemporary); + }} + > + + + { + e.stopPropagation(); + + if (canOpenModal || isTemporary) { + setIsOpen(true); + } else { + strapi.notification.info( + `${pluginId}.notification.info.work.notSaved` + ); + } + }} + > + + + + setIsOpen(prevState => !prevState)} + content={{ + message: `${pluginId}.popUpWarning.bodyMessage.${ + type === 'models' ? 'contentType' : 'groups' + }.delete`, + }} + type="danger" + onConfirm={() => { + setIsOpen(false); + deleteAttribute(name); + }} + /> + > + )} + + + > + ); +} + +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; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/actions.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/actions.js index df457d97a0..b0b08e752f 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/actions.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/actions.js @@ -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, diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/constants.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/constants.js index 73adeaab3e..5282da7edd 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/constants.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/constants.js @@ -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'; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/reducer.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/reducer.js index deb000847b..8758d6c062 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/reducer.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/reducer.js @@ -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 diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/GroupPage/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/GroupPage/index.js index 66beb10008..f87aa5a73b 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/GroupPage/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/GroupPage/index.js @@ -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 ( <> @@ -89,14 +144,47 @@ class GroupPage extends React.Component { headerTitle={this.getFeatureHeaderTitle()} headerDescription={this.getFeatureHeaderDescription()} > - + {attributesNumber === 0 ? ( + + ) : ( + + + + + + {attributes.map(attribute => ( + {}} + /> + ))} + + + + + + + + )} ', () => { shallow(); }); + describe('CTB 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(); + + 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 ', () => { }); }); - describe('getFeatureName', () => { + describe('GetFeature', () => { + it('should return the correct model', () => { + const { getFeature } = shallow().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().instance(); + + expect(getFeature()).toEqual(props.newGroup); + }); + }); + + describe('GetFeatureName', () => { it("should return the model's name field", () => { const { getFeatureName } = shallow().instance(); @@ -107,3 +137,37 @@ describe('CTB ', () => { }); }); }); + +describe('CTB , 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( + + ).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( + + ).instance(); + + handleDeleteGroupAttribute('name'); + const keys = ['newGroup', 'schema', 'attributes', 'name']; + expect(props.deleteGroupAttribute).toHaveBeenCalledWith(keys); + }); +}); diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json index bf06c10a28..46ae11fb91 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json @@ -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." } diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json index 9c5b9dca0c..255b14276f 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json @@ -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." } diff --git a/packages/strapi-plugin-content-type-builder/controllers/Fixtures.js b/packages/strapi-plugin-content-type-builder/controllers/Fixtures.js index 8f6dd8502e..ec18c0dc7e 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/Fixtures.js +++ b/packages/strapi-plugin-content-type-builder/controllers/Fixtures.js @@ -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: {}, }, }, //...
+ {name} + {source && ( + + {message => ( + + ({message}: {source}) + + )} + + )} + + {isTemporary && ( + + )} +