diff --git a/packages/strapi-plugin-content-type-builder/admin/src/components/AttributeLi/index.js b/packages/strapi-plugin-content-type-builder/admin/src/components/AttributeLi/index.js index 5e8838b775..e6c61e0063 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/components/AttributeLi/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/components/AttributeLi/index.js @@ -53,7 +53,14 @@ function AttributeLi({ const configurableStyle = configurable === false ? null : styles.editable; return ( -
  • +
  • { + if (configurable !== false) { + onClick(name, type); + } + }} + >
    {`icon-${ico}`} 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 be8e5230a2..052e48d9e4 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 @@ -23,6 +23,8 @@ import { RESET_EXISTING_CONTENT_TYPE_MAIN_INFOS, RESET_EDIT_TEMP_CONTENT_TYPE, RESET_PROPS, + SAVE_EDITED_ATTRIBUTE, + SET_TEMPORARY_ATTRIBUTE, SUBMIT_TEMP_CONTENT_TYPE, SUBMIT_TEMP_CONTENT_TYPE_SUCCEEDED, UPDATE_TEMP_CONTENT_TYPE, @@ -140,6 +142,24 @@ export function onCreateAttribute({ target }) { }; } +export function saveEditedAttribute(attributeName, isModelTemporary, modelName) { + return { + type: SAVE_EDITED_ATTRIBUTE, + attributeName, + isModelTemporary, + modelName, + }; +} + +export function setTemporaryAttribute(attributeName, isModelTemporary, modelName) { + return { + type: SET_TEMPORARY_ATTRIBUTE, + attributeName, + isModelTemporary, + modelName, + }; +} + export function resetNewContentTypeMainInfos() { return { type: RESET_NEW_CONTENT_TYPE_MAIN_INFOS, 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 652ec282e4..ef47c7cdaf 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 @@ -28,6 +28,8 @@ export const RESET_EXISTING_CONTENT_TYPE_MAIN_INFOS = 'ContentTypeBuilder/App/RESET_EXISTING_CONTENT_TYPE_MAIN_INFOS'; export const RESET_EDIT_TEMP_CONTENT_TYPE = 'ContentTypeBuilder/App/RESET_EDIT_TEMP_CONTENT_TYPE'; export const RESET_PROPS = 'ContentTypeBuilder/App/RESET_PROPS'; +export const SAVE_EDITED_ATTRIBUTE = 'ContentTypeBuilder/App/SAVE_EDITED_ATTRIBUTE'; +export const SET_TEMPORARY_ATTRIBUTE = 'ContentTypeBuilder/App/SET_TEMPORARY_ATTRIBUTE'; export const SUBMIT_TEMP_CONTENT_TYPE = 'ContentTypeBuilder/App/SUBMIT_TEMP_CONTENT_TYPE'; export const SUBMIT_TEMP_CONTENT_TYPE_SUCCEEDED = 'ContentTypeBuilder/App/SUBMIT_TEMP_CONTENT_TYPE_SUCCEEDED'; export const UPDATE_TEMP_CONTENT_TYPE = 'ContentTypeBuilder/App/UPDATE_TEMP_CONTENT_TYPE'; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/index.js index be9db2fbe3..6059badcdb 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/index.js @@ -29,6 +29,8 @@ import { resetExistingContentTypeMainInfos, resetNewContentTypeMainInfos, resetProps, + saveEditedAttribute, + setTemporaryAttribute, updateTempContentType, } from './actions'; @@ -106,6 +108,7 @@ App.propTypes = { onChangeExistingContentTypeMainInfos: PropTypes.func.isRequired, onChangeNewContentTypeMainInfos: PropTypes.func.isRequired, resetProps: PropTypes.func.isRequired, + saveEditedAttribute: PropTypes.func.isRequired, }; const mapStateToProps = makeSelectApp(); @@ -123,6 +126,8 @@ export function mapDispatchToProps(dispatch) { resetExistingContentTypeMainInfos, resetNewContentTypeMainInfos, resetProps, + saveEditedAttribute, + setTemporaryAttribute, updateTempContentType, }, dispatch, 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 485ae0a54d..01ffd7afd5 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 @@ -24,6 +24,8 @@ import { RESET_EDIT_EXISTING_CONTENT_TYPE, RESET_EDIT_TEMP_CONTENT_TYPE, RESET_PROPS, + SAVE_EDITED_ATTRIBUTE, + SET_TEMPORARY_ATTRIBUTE, SUBMIT_TEMP_CONTENT_TYPE_SUCCEEDED, UPDATE_TEMP_CONTENT_TYPE, } from './constants'; @@ -158,6 +160,25 @@ function appReducer(state = initialState, action) { }); case RESET_PROPS: return initialState; + case SAVE_EDITED_ATTRIBUTE: { + const basePath = action.isModelTemporary ? ['newContentType'] : ['modifiedData', action.modelName]; + + return state.updateIn([...basePath, 'attributes'], attributes => { + const temporaryAttribute = state.get('temporaryAttribute'); + const newAttribute = temporaryAttribute.remove('name'); + + return attributes.remove(action.attributeName).set(temporaryAttribute.get('name'), newAttribute); + }); + } + case SET_TEMPORARY_ATTRIBUTE: + return state.update('temporaryAttribute', () => { + const basePath = action.isModelTemporary ? ['newContentType'] : ['modifiedData', action.modelName]; + const attribute = state + .getIn([...basePath, 'attributes', action.attributeName]) + .set('name', action.attributeName); + + return attribute; + }); case SUBMIT_TEMP_CONTENT_TYPE_SUCCEEDED: return state .updateIn(['initialData', state.getIn(['newContentType', 'name'])], () => state.get('newContentType')) diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/tests/index.test.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/tests/index.test.js index 2b936ad33a..684646bce7 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/App/tests/index.test.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/App/tests/index.test.js @@ -46,6 +46,7 @@ describe('', () => { modifiedData: {}, onChangeExistingContentTypeMainInfos: jest.fn(), onChangeNewContentTypeMainInfos: jest.fn(), + saveEditedAttribute: jest.fn(), resetProps: jest.fn(), }; }); diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/index.js index 2eaddd41a2..5bd4ad6a29 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/index.js @@ -40,15 +40,17 @@ class AttributeForm extends React.Component { }; getFormErrors = () => { - const { alreadyTakenAttributes, modifiedData } = this.props; + const { alreadyTakenAttributes, attributeToEditName, modifiedData } = this.props; const currentForm = this.getCurrentForm(); let formErrors = {}; - + const alreadyTakenAttributesUpdated = alreadyTakenAttributes.filter( + attribute => attribute !== attributeToEditName, + ); if (isEmpty(modifiedData.name)) { formErrors = { name: [{ id: `${pluginId}.error.validation.required` }] }; } - if (alreadyTakenAttributes.includes(get(modifiedData, 'name', ''))) { + if (alreadyTakenAttributesUpdated.includes(get(modifiedData, 'name', ''))) { formErrors = { name: [{ id: `${pluginId}.error.attribute.taken` }] }; } @@ -81,10 +83,10 @@ class AttributeForm extends React.Component { }; handleGoTo = to => { - const { attributeType, push } = this.props; + const { actionType, attributeType, push } = this.props; push({ - search: `modalType=attributeForm&attributeType=${attributeType}&settingType=${to}&actionType=create`, + search: `modalType=attributeForm&attributeType=${attributeType}&settingType=${to}&actionType=${actionType}`, }); }; @@ -99,7 +101,11 @@ class AttributeForm extends React.Component { handleSubmit = () => { if (isEmpty(this.getFormErrors())) { - this.props.onSubmit(); + if (this.props.actionType === 'create') { + this.props.onSubmit(); + } else { + this.props.onSubmitEdit(); + } } }; @@ -107,7 +113,11 @@ class AttributeForm extends React.Component { e.preventDefault(); if (isEmpty(this.getFormErrors())) { - this.props.onSubmit(true); + if (this.props.actionType === 'create') { + this.props.onSubmit(true); + } else { + this.props.onSubmitEdit(true); + } } }; @@ -166,9 +176,10 @@ class AttributeForm extends React.Component { }; render() { - const { attributeType, isOpen } = this.props; + const { actionType, attributeToEditName, attributeType, isOpen } = this.props; const { showForm } = this.state; const currentForm = this.getCurrentForm(); + const titleContent = actionType === 'create' ? attributeType : attributeToEditName; return (
    - +   - {attributeType} + {titleContent}  
    @@ -205,7 +216,9 @@ class AttributeForm extends React.Component { } AttributeForm.defaultProps = { + actionType: 'create', activeTab: 'base', + attributeToEditName: '', alreadyTakenAttributes: [], attributeType: 'string', isOpen: false, @@ -216,14 +229,17 @@ AttributeForm.defaultProps = { }; AttributeForm.propTypes = { + actionType: PropTypes.string, activeTab: PropTypes.string, alreadyTakenAttributes: PropTypes.array, + attributeToEditName: PropTypes.string, attributeType: PropTypes.string, isOpen: PropTypes.bool, modifiedData: PropTypes.object, // TODO: Clearly define this object (It's working without it though) onCancel: PropTypes.func, onChange: PropTypes.func, onSubmit: PropTypes.func.isRequired, + onSubmitEdit: PropTypes.func.isRequired, push: PropTypes.func, }; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/tests/index.test.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/tests/index.test.js index 3358cb6338..3205bec326 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/tests/index.test.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributeForm/tests/index.test.js @@ -19,6 +19,7 @@ describe('', () => { beforeEach(() => { props = { onSubmit: jest.fn(), + onSubmitEdit: jest.fn(), }; }); diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/index.js index 5a8bf225c4..13fc71bd13 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/index.js @@ -19,7 +19,8 @@ import WrapperModal from '../../components/WrapperModal'; import attributes from './attributes.json'; -class AttributesPickerModal extends React.Component { // eslint-disable-line react/prefer-stateless-function +class AttributesPickerModal extends React.Component { + // eslint-disable-line react/prefer-stateless-function state = { isDisplayed: false, nodeToFocus: 0 }; componentDidMount() { @@ -55,24 +56,24 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea return attr.type !== 'media'; }); - } + }; addEventListener = () => { document.addEventListener('keydown', this.handleKeyDown); - } + }; removeEventListener = () => { document.removeEventListener('keydown', this.handleKeyDown); - } + }; - handleClick = (type) => { + handleClick = type => { const { push } = this.props; - push({ search: `modalType=attributeForm&attributeType=${type}&settingType=base` }); - } + push({ search: `modalType=attributeForm&attributeType=${type}&settingType=base&actionType=create` }); + }; /* istanbul ignore next */ - handleKeyDown = (e) => { + handleKeyDown = e => { const { push } = this.props; /* istanbul ignore next */ @@ -101,7 +102,9 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea e.preventDefault(); push({ - search: `modalType=attributeForm&attributeType=${attributes[nodeToFocus].type}&settingType=base`, + search: `modalType=attributeForm&attributeType=${ + attributes[nodeToFocus].type + }&settingType=base&actionType=create`, }); break; default: @@ -110,7 +113,7 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea /* istanbul ignore next */ this.updateNodeToFocus(next); - } + }; handleOnClosed = () => this.setState(prevState => ({ isDisplayed: !prevState.isDisplayed })); @@ -120,7 +123,7 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea const { push } = this.props; push({ search: '' }); - } + }; updateNodeToFocus = position => this.setState({ nodeToFocus: position }); @@ -138,7 +141,7 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea {...attribute} /> ); - } + }; render() { const { isOpen } = this.props; @@ -151,13 +154,9 @@ class AttributesPickerModal extends React.Component { // eslint-disable-line rea onOpened={this.handleOnOpened} > - + - - {attributes.map(this.renderAttribute)} - + {attributes.map(this.renderAttribute)}
    ); diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/tests/index.test.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/tests/index.test.js index bad0b00d23..e08ad31180 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/tests/index.test.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/AttributesPickerModal/tests/index.test.js @@ -11,7 +11,8 @@ import AttributesPickerModal from '../index'; const messages = formatMessagesWithPluginId(pluginId, pluginTradsEn); -const renderComponent = (props = {}, context = {}) => mountWithIntl(, messages, context); +const renderComponent = (props = {}, context = {}) => + mountWithIntl(, messages, context); describe('', () => { let props; @@ -80,8 +81,8 @@ describe('', () => { const { getAttributes } = wrapper.instance(); expect(getAttributes()).not.toContain({ - "type": "media", - "description": "content-type-builder.popUpForm.attributes.media.description", + type: 'media', + description: 'content-type-builder.popUpForm.attributes.media.description', }); }); @@ -89,15 +90,15 @@ describe('', () => { const context = { plugins: { 'content-type-builder': {}, - 'upload': {}, + upload: {}, }, }; const wrapper = renderComponent(props, context); const { getAttributes } = wrapper.instance(); expect(getAttributes()).toContainEqual({ - "type": "media", - "description": "content-type-builder.popUpForm.attributes.media.description", + type: 'media', + description: 'content-type-builder.popUpForm.attributes.media.description', }); wrapper.unmount(); @@ -107,7 +108,7 @@ describe('', () => { const context = { plugins: fromJS({ 'content-type-builder': {}, - 'upload': {}, + upload: {}, }), }; const wrapper = renderComponent(props, context); @@ -154,7 +155,7 @@ describe('', () => { const context = { plugins: fromJS({ 'content-type-builder': {}, - 'upload': {}, + upload: {}, }), }; const wrapper = renderComponent(props, context); @@ -175,7 +176,9 @@ describe('', () => { handleClick('test'); - expect(props.push).toHaveBeenCalledWith({ search: 'modalType=attributeForm&attributeType=test&settingType=base' }); + expect(props.push).toHaveBeenCalledWith({ + search: 'modalType=attributeForm&attributeType=test&settingType=base&actionType=create', + }); }); }); }); diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/ModelPage/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/ModelPage/index.js index 39a1b39576..9e02eaae21 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/ModelPage/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/ModelPage/index.js @@ -62,6 +62,22 @@ export class ModelPage extends React.Component { // eslint-disable-line react/prefer-stateless-function state = { attrToDelete: null, removePrompt: false, showWarning: false }; + componentDidMount() { + const { setTemporaryAttribute } = this.props; + + if ( + this.getModalType() === 'attributeForm' && + this.getActionType() === 'edit' && + !this.isTryingToEditAnUnknownAttribute() + ) { + setTemporaryAttribute( + this.getAttributeName(), + this.isUpdatingTemporaryContentType(), + this.getModelName(), + ); + } + } + componentDidUpdate(prevProps) { const { location: { search }, @@ -80,20 +96,22 @@ export class ModelPage extends React.Component { } } - getFormData = () => { - const { - location: { search }, - modifiedData, - newContentType, - } = this.props; + getActionType = () => getQueryParameters(this.getSearch(), 'actionType'); - if (getQueryParameters(search, 'actionType') === 'create' || this.isUpdatingTemporaryContentType()) { + getAttributeName = () => getQueryParameters(this.getSearch(), 'attributeName'); + + getFormData = () => { + const { modifiedData, newContentType } = this.props; + + if (this.getActionType() === 'create' || this.isUpdatingTemporaryContentType()) { return newContentType; } return get(modifiedData, this.getModelName()); }; + getModalType = () => getQueryParameters(this.getSearch(), 'modalType'); + getModel = () => { const { modifiedData, newContentType } = this.props; @@ -194,14 +212,41 @@ export class ModelPage extends React.Component { return title; }; + getSearch = () => { + const { + location: { search }, + } = this.props; + + return search; + }; + getSectionTitle = () => { const base = `${pluginId}.menu.section.contentTypeBuilder.name.`; return this.getModelsNumber() > 1 ? `${base}plural` : `${base}singular`; }; + handleClickEditAttribute = async (attributeName, type) => { + // modalType=attributeForm&attributeType=boolean&settingType=base + const { + history: { push }, + } = this.props; + const attributeType = ['integer', 'biginteger', 'float', 'decimal'].includes(type) ? 'number' : type; + + this.props.setTemporaryAttribute( + attributeName, + this.isUpdatingTemporaryContentType(), + this.getModelName(), + ); + await this.wait(); + push({ + search: `modalType=attributeForm&attributeType=${attributeType}&settingType=base&actionType=edit&attributeName=${attributeName}`, + }); + }; + handleClickEditModelMainInfos = async () => { const { canOpenModal } = this.props; + await this.wait(); if (canOpenModal || this.isUpdatingTemporaryContentType()) { @@ -284,15 +329,25 @@ export class ModelPage extends React.Component { push({ search: nextSearch }); }; - hasModelBeenModified = () => { + handleSubmitEdit = (shouldContinue = false) => { const { - initialData, - location: { search }, - modifiedData, + history: { push }, + saveEditedAttribute, } = this.props; + const attributeName = this.getAttributeName(); + + saveEditedAttribute(attributeName, this.isUpdatingTemporaryContentType(), this.getModelName()); + + const nextSearch = shouldContinue ? 'modalType=chooseAttributes' : ''; + + push({ search: nextSearch }); + }; + + hasModelBeenModified = () => { + const { initialData, modifiedData } = this.props; const currentModel = this.getModelName(); - return !isEqual(initialData[currentModel], modifiedData[currentModel]) && search === ''; + return !isEqual(initialData[currentModel], modifiedData[currentModel]) && this.getSearch() === ''; }; isUpdatingTemporaryContentType = (modelName = this.getModelName()) => { @@ -307,10 +362,19 @@ export class ModelPage extends React.Component { setPrompt = () => this.setState({ removePrompt: false }); + isTryingToEditAnUnknownAttribute = () => { + const hasAttribute = Object.keys(this.getModelAttributes()).indexOf(this.getAttributeName()) !== -1; + + return this.getActionType() === 'edit' && this.getModalType() === 'attributeForm' && !hasAttribute; + }; + shouldRedirect = () => { const { models } = this.props; - return models.findIndex(model => model.name === this.getModelName()) === -1; + return ( + models.findIndex(model => model.name === this.getModelName()) === -1 || + this.isTryingToEditAnUnknownAttribute() + ); }; toggleModalWarning = () => this.setState(prevState => ({ showWarning: !prevState.showWarning })); @@ -353,6 +417,7 @@ export class ModelPage extends React.Component { key={attribute} name={attribute} attributeInfos={attributeInfos} + onClick={this.handleClickEditAttribute} onClickOnTrashIcon={this.handleClickOnTrashIcon} /> ); @@ -385,10 +450,11 @@ export class ModelPage extends React.Component { return ; } - const modalType = getQueryParameters(search, 'modalType'); + // const modalType = getQueryParameters(search, 'modalType'); + const modalType = this.getModalType(); const settingType = getQueryParameters(search, 'settingType'); const attributeType = getQueryParameters(search, 'attributeType'); - const actionType = getQueryParameters(search, 'actionType'); + const actionType = this.getActionType(); return (
    @@ -473,15 +539,18 @@ export class ModelPage extends React.Component {