Handle add attributes to content type

This commit is contained in:
cyril lopez 2017-08-30 16:38:58 +02:00
parent 7bf1f3183c
commit 28f35f678b
17 changed files with 204 additions and 40 deletions

View File

@ -8,6 +8,7 @@ import React from 'react';
import { isEmpty, startCase } from 'lodash';
import { FormattedMessage } from 'react-intl';
import { router } from 'app';
import Button from 'components/Button';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/no-static-element-interactions */
@ -16,20 +17,35 @@ class ContentHeader extends React.Component { // eslint-disable-line react/prefe
router.push(this.props.editPath);
}
renderButtonContainer = () => (
<div className={styles.buttonContainer}>
<FormattedMessage id="form.button.cancel">
{(message) => (
<Button type="button" label={message} buttonSize={"buttonMd"} buttonBackground={"secondary"} onClick={this.props.handleCancel} />
)}
</FormattedMessage>
<FormattedMessage id="form.button.save">
{(message) => (
<Button type="submit" label={message} buttonSize={"buttonLg"} buttonBackground={"primary"} onClick={this.props.handleSubmit} />
)}
</FormattedMessage>
</div>
)
renderContentHeader = () => {
const containerClass = this.props.noMargin ? styles.contentHeaderNoMargin : styles.contentHeader;
const editIcon = this.props.editIcon ?
<i className="fa fa-pencil" onClick={this.edit} role="button" />
: '';
const description = this.props.description || <FormattedMessage id="modelPage.contentHeader.emptyDescription.description" />
const description = this.props.description || <FormattedMessage id="modelPage.contentHeader.emptyDescription.description" />;
const buttons = this.props.addButtons ? this.renderButtonContainer() : '';
return (
<div className={containerClass}>
<div className={`${styles.title} ${styles.flex}`}>
<span>{startCase(this.props.name)}</span>
{editIcon}
<div>
<div className={`${styles.title} ${styles.flex}`}>
<span>{startCase(this.props.name)}</span>
<i className={`fa fa-${this.props.icoType}`} onClick={this.edit} role="button" />
</div>
<div className={styles.subTitle}>{description}</div>
</div>
<div className={styles.subTitle}>{description}</div>
{buttons}
</div>
);
}
@ -37,25 +53,32 @@ class ContentHeader extends React.Component { // eslint-disable-line react/prefe
render() {
const containerClass = this.props.noMargin ? styles.contentHeaderNoMargin : styles.contentHeader;
const description = isEmpty(this.props.description) ? '' : <FormattedMessage id={this.props.description} />;
const buttons = this.props.addButtons ? this.renderButtonContainer() : '';
if (this.props.noI18n) return this.renderContentHeader();
if (this.props.editIcon) return this.renderContentHeader();
return (
<div className={containerClass}>
<div className={styles.title}>
<FormattedMessage id={this.props.name} />
<div>
<div className={styles.title}>
<FormattedMessage id={this.props.name} />
</div>
<div className={styles.subTitle}>{description}</div>
</div>
<div className={styles.subTitle}>{description}</div>
{buttons}
</div>
);
}
}
ContentHeader.propTypes = {
addButtons: React.PropTypes.bool,
description: React.PropTypes.string,
editIcon: React.PropTypes.bool,
editPath: React.PropTypes.string,
handleCancel: React.PropTypes.func,
handleSubmit: React.PropTypes.func,
icoType: React.PropTypes.string,
name: React.PropTypes.string,
noI18n: React.PropTypes.bool,
noMargin: React.PropTypes.bool,
};

View File

@ -2,12 +2,16 @@
position: relative;
margin: 2.4rem 0rem 3.3rem 0rem;
font-family: Lato;
display: flex;
justify-content: space-between;
}
.contentHeaderNoMargin {
position: relative;
margin-bottom: 3.2rem;
font-family: Lato;
display: flex;
justify-content: space-between;
}
.title {
@ -46,3 +50,9 @@
}
// color: #101622;
}
.buttonContainer {
> button:last-child {
margin-right: 0;
}
}

View File

@ -16,6 +16,7 @@ import {
MODELS_FETCH,
MODELS_FETCH_SUCCEEDED,
STORE_TEMPORARY_MENU,
TEMPORARY_CONTENT_TYPE_POSTED,
} from './constants';
export function deleteContentType(itemToDelete) {
@ -90,3 +91,9 @@ export function storeTemporaryMenu(newMenu, position, nbElementToRemove) {
nbElementToRemove,
};
}
export function temporaryContentTypePosted() {
return {
type: TEMPORARY_CONTENT_TYPE_POSTED,
};
}

View File

@ -8,3 +8,4 @@ export const DELETE_CONTENT_TYPE = 'ContentTypeBuilder/App/DELETE_CONTENT_TYPE';
export const MODELS_FETCH = 'ContentTypeBuilder/App/MODELS_FETCH';
export const MODELS_FETCH_SUCCEEDED = 'ContentTypeBuilder/App/MODELS_FETCH_SUCCEEDED';
export const STORE_TEMPORARY_MENU = 'ContentTypeBuilder/App/STORE_TEMPORARY_MENU';
export const TEMPORARY_CONTENT_TYPE_POSTED = 'ContentTypeBuilder/App/TEMPORARY_CONTENT_TYPE_POSTED';

View File

@ -21,6 +21,7 @@ import messages from '../../translations/en.json';
import styles from './styles.scss';
import { modelsFetch } from './actions';
import { makeSelectMenu } from './selectors';
define(map(messages, (message, id) => ({
id,
@ -50,12 +51,13 @@ class App extends React.Component {
const content = React.Children.map(this.props.children, child =>
React.cloneElement(child, {
exposedComponents: this.props.exposedComponents,
menu: this.props.menu,
})
);
return (
<div className={`${pluginId} ${styles.app}`}>
{React.Children.toArray(content)}
{React.Children.toArray(content, 'fuck')}
</div>
);
}
@ -68,6 +70,7 @@ App.contextTypes = {
App.propTypes = {
children: React.PropTypes.node,
exposedComponents: React.PropTypes.object.isRequired,
menu: React.PropTypes.array,
modelsFetch: React.PropTypes.func,
shouldRefetchContentType: React.PropTypes.bool,
};
@ -82,6 +85,7 @@ export function mapDispatchToProps(dispatch) {
}
const mapStateToProps = createStructuredSelector({
menu: makeSelectMenu(),
shouldRefetchContentType: makeSelectShouldRefetchContentType(),
});

View File

@ -11,6 +11,7 @@ import {
MODELS_FETCH,
MODELS_FETCH_SUCCEEDED,
STORE_TEMPORARY_MENU,
TEMPORARY_CONTENT_TYPE_POSTED,
} from './constants';
/* eslint-disable new-cap */
@ -39,6 +40,13 @@ function appReducer(state = initialState, action) {
.updateIn(['menu', '0', 'items'], (list) => list.splice(action.position, action.nbElementToRemove, action.newLink))
.update('models', array => array.splice(action.nbElementToRemove === 0 ? modelsSize : modelsSize - 1 , 1, action.newModel));
}
case TEMPORARY_CONTENT_TYPE_POSTED: {
const oldMenuItem = state.getIn(['menu', '0', 'items', size(state.getIn(['menu', '0', 'items']).toJS()) -2]);
oldMenuItem.isTemporary = false;
const newData = oldMenuItem;
return state
.updateIn(['menu', '0', 'items', size(state.getIn(['menu', '0', 'items']).toJS()) -2], () => newData);
}
default:
return state;
}

View File

@ -34,14 +34,9 @@ export function* fetchModels() {
// Individual exports for testing
export function* defaultSaga() {
// TODO check if problems
yield fork(takeLatest, MODELS_FETCH, fetchModels);
yield fork(takeLatest, DELETE_CONTENT_TYPE, deleteContentType);
// const loadModelsWatcher = yield fork(takeLatest, MODELS_FETCH, fetchModels);
yield fork(takeLatest, MODELS_FETCH, fetchModels);
// Suspend execution until location changes
// yield take(MODELS_FETCH_SUCCEEDED);
// yield cancel(loadModelsWatcher);
}
// All sagas to be loaded

View File

@ -21,6 +21,7 @@ import {
CONTENT_TYPE_FETCH,
CONTENT_TYPE_FETCH_SUCCEEDED,
RESET_DID_FETCH_MODEL_PROP,
RESET_IS_FORM_SET,
SET_ATTRIBUTE_FORM,
SET_FORM,
} from './constants';
@ -112,6 +113,12 @@ export function resetDidFetchModelProp() {
};
}
export function resetIsFormSet() {
return {
type: RESET_IS_FORM_SET,
};
}
export function setAttributeForm(hash) {
const hashArray = hash.split('::');
const formType = replace(hashArray[1], 'attribute', '');
@ -122,10 +129,10 @@ export function setAttributeForm(hash) {
name: '',
params: Map({
type: formType,
required: false,
required: true,
// TODO remove with correct value
minLength: true,
minLengthValue: 0,
// minLength: true,
// minLengthValue: 0,
}),
});

View File

@ -14,5 +14,6 @@ export const CONTENT_TYPE_EDIT = 'ContentTypeBuilder/Form/CONTENT_TYPE_EDIT';
export const CONTENT_TYPE_FETCH = 'ContentTypeBuilder/Form/CONTENT_TYPE_FETCH';
export const CONTENT_TYPE_FETCH_SUCCEEDED = 'ContentTypeBuilder/Form/CONTENT_TYPE_FETCH_SUCCEEDED';
export const RESET_DID_FETCH_MODEL_PROP = 'ContentTypeBuilder/Form/RESET_DID_FETCH_MODEL_PROP';
export const RESET_IS_FORM_SET = 'ContentTypeBuilder/Form/RESET_IS_FORM_SET';
export const SET_ATTRIBUTE_FORM = 'ContentTypeBuilder/Form/SET_ATTRIBUTE_FORM';
export const SET_FORM = 'ContentTypeBuilder/Form/SET_FORM';

View File

@ -23,7 +23,7 @@ import {
import { router, store } from 'app';
import { storeTemporaryMenu } from 'containers/App/actions';
import { addAttributeToContentType } from 'containers/ModelPage/actions';
import AttributeCard from 'components/AttributeCard';
import InputCheckboxWithNestedInputs from 'components/InputCheckboxWithNestedInputs';
import PopUpForm from 'components/PopUpForm';
@ -44,6 +44,7 @@ import {
contentTypeFetch,
contentTypeFetchSucceeded,
resetDidFetchModelProp,
resetIsFormSet,
setAttributeForm,
setForm,
} from './actions';
@ -100,6 +101,13 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
}
}
addAttributeToContentType = () => {
this.props.addAttributeToContentType(this.props.modifiedDataAttribute);
// this.props.resetDidFetchModelProp();
this.props.resetIsFormSet();
router.push(`${this.props.redirectRoute}/${replace(this.props.hash.split('::')[0], '#create', '')}`);
}
addAttributeToTempContentType = () => {
const contentType = this.props.modifiedDataEdit;
contentType.attributes.push(this.props.modifiedDataAttribute);
@ -209,6 +217,8 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
this.testContentType(
replace(split(this.props.hash, '::')[0], '#create', ''),
this.addAttributeToTempContentType,
null,
this.addAttributeToContentType,
);
} else {
this.createContentType(this.props.modifiedData);
@ -263,7 +273,7 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
this.props.toggle();
// Set the didFetchModel props to false when the modal is closing so the store is emptied
if (this.state.showModal) {
this.props.resetDidFetchModelProp();
this.props.resetIsFormSet();
}
}
@ -327,6 +337,7 @@ const mapStateToProps = selectForm();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
addAttributeToContentType,
changeInput,
changeInputAttribute,
connectionsFetch,
@ -335,6 +346,7 @@ function mapDispatchToProps(dispatch) {
contentTypeFetch,
contentTypeFetchSucceeded,
resetDidFetchModelProp,
resetIsFormSet,
setAttributeForm,
setForm,
storeTemporaryMenu,
@ -344,6 +356,7 @@ function mapDispatchToProps(dispatch) {
}
Form.propTypes = {
addAttributeToContentType: React.PropTypes.func,
changeInput: React.PropTypes.func.isRequired,
changeInputAttribute: React.PropTypes.func,
connectionsFetch: React.PropTypes.func.isRequired,
@ -365,6 +378,7 @@ Form.propTypes = {
popUpHeaderNavLinks: React.PropTypes.array,
redirectRoute: React.PropTypes.string.isRequired,
resetDidFetchModelProp: React.PropTypes.func,
resetIsFormSet: React.PropTypes.func,
routePath: React.PropTypes.string,
selectOptions: React.PropTypes.array,
selectOptionsFetchSucceeded: React.PropTypes.bool,

View File

@ -13,6 +13,7 @@ import {
CONTENT_TYPE_CREATE,
CONTENT_TYPE_FETCH_SUCCEEDED,
RESET_DID_FETCH_MODEL_PROP,
RESET_IS_FORM_SET,
SET_ATTRIBUTE_FORM,
SET_FORM,
} from './constants';
@ -67,6 +68,8 @@ function formReducer(state = initialState, action) {
return state
.set('didFetchModel', true)
.set('isFormSet', false);
case RESET_IS_FORM_SET:
return state.set('isFormSet', false);
case SET_ATTRIBUTE_FORM: {
if (state.get('isFormSet')) {
return state.set('form', Map(action.form));

View File

@ -7,12 +7,29 @@ import { get } from 'lodash';
import { storeData } from '../../utils/storeData';
import {
ADD_ATTRIBUTE_TO_CONTENT_TYPE,
CANCEL_CHANGES,
DEFAULT_ACTION,
DELETE_ATTRIBUTE,
MODEL_FETCH,
MODEL_FETCH_SUCCEEDED,
POST_CONTENT_TYPE_SUCCEEDED,
SUBMIT,
} from './constants';
export function addAttributeToContentType(newAttribute) {
return {
type: ADD_ATTRIBUTE_TO_CONTENT_TYPE,
newAttribute,
};
}
export function cancelChanges() {
return {
type: CANCEL_CHANGES,
};
}
export function deleteAttribute(position, modelName) {
const temporaryContentType = storeData.getContentType();
let sendRequest = true;
@ -50,3 +67,15 @@ export function modelFetchSucceeded(model) {
model,
};
}
export function postContentTypeSucceeded() {
return {
type: POST_CONTENT_TYPE_SUCCEEDED,
};
}
export function submit() {
return {
type: SUBMIT,
}
}

View File

@ -4,7 +4,11 @@
*
*/
export const ADD_ATTRIBUTE_TO_CONTENT_TYPE = 'ContentTypeBuilder/ModelPage/ADD_ATTRIBUTE_TO_CONTENT_TYPE';
export const CANCEL_CHANGES = 'ContentTypeBuilder/ModelPage/CANCEL_CHANGES';
export const DEFAULT_ACTION = 'ContentTypeBuilder/ModelPage/DEFAULT_ACTION';
export const DELETE_ATTRIBUTE = 'ContentTypeBuilder/ModelPage/DELETE_ATTRIBUTE';
export const MODEL_FETCH = 'ContentTypeBuilder/ModelPage/MODEL_FETCH';
export const MODEL_FETCH_SUCCEEDED = 'ContentTypeBuilder/ModelPage/MODEL_FETCH_SUCCEEDED';
export const POST_CONTENT_TYPE_SUCCEEDED = 'ContentTypeBuilder/ModelPage/POST_CONTENT_TYPE_SUCCEEDED';
export const SUBMIT = 'ContentTypeBuilder/ModelPage/SUBMIT';

View File

@ -14,7 +14,6 @@ import { Link } from 'react-router';
import { router } from 'app';
// Global selectors
import { makeSelectMenu } from 'containers/App/selectors';
import { makeSelectDidFetchModel } from 'containers/Form/selectors';
import AttributeRow from 'components/AttributeRow';
@ -27,9 +26,11 @@ import PluginLeftMenu from 'components/PluginLeftMenu';
import { storeData } from '../../utils/storeData';
import {
cancelChanges,
deleteAttribute,
modelFetch,
modelFetchSucceeded,
submit,
} from './actions';
import selectModelPage from './selectors';
@ -88,11 +89,6 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
</div>
)
handleEditAttribute = (attributeName) => {
const index = findIndex(this.props.modelPage.model.attributes, ['name', attributeName]);
console.log(index);
}
fetchModel = () => {
if (storeData.getIsModelTemporary() && get(storeData.getContentType(), 'name') === this.props.params.modelName) {
this.props.modelFetchSucceeded({ model: storeData.getContentType() });
@ -118,6 +114,12 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
this.props.deleteAttribute(index, this.props.params.modelName);
}
handleEditAttribute = (attributeName) => {
const index = findIndex(this.props.modelPage.model.attributes, ['name', attributeName]);
console.log(index);
}
toggleModal = () => {
const locationHash = this.props.location.hash ? '' : '#create::contentType::baseSettings';
router.push(`plugins/content-type-builder/models/${this.props.params.modelName}${locationHash}`);
@ -185,6 +187,8 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
render() {
// Url to redirects the user if he modifies the temporary content type name
const redirectRoute = replace(this.props.route.path, '/:modelName', '');
const addButtons = size(get(this.props.modelPage.model, 'attributes')) > 0;
const content = size(this.props.modelPage.model.attributes) === 0 ?
<EmptyAttributesView handleClick={this.handleClickAddAttribute} /> :
<List
@ -194,7 +198,6 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
renderCustomLi={this.renderCustomLi}
handleButtonClick={this.handleClickAddAttribute}
/>;
return (
<div className={styles.modelPage}>
<div className="container-fluid">
@ -209,9 +212,12 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
<ContentHeader
name={this.props.modelPage.model.name}
description={this.props.modelPage.model.description}
noI18n
icoType="pencil"
editIcon
editPath={`${redirectRoute}/${this.props.params.modelName}#edit${this.props.params.modelName}::contentType::baseSettings`}
addButtons={addButtons}
handleSubmit={this.props.submit}
handleCancel={this.props.cancelChanges}
/>
{content}
</div>
@ -234,22 +240,24 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
const mapStateToProps = createStructuredSelector({
modelPage: selectModelPage(),
menu: makeSelectMenu(),
didFetchModel: makeSelectDidFetchModel(),
});
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
cancelChanges,
deleteAttribute,
modelFetch,
modelFetchSucceeded,
submit,
},
dispatch,
);
}
ModelPage.propTypes = {
cancelChanges: React.PropTypes.func,
deleteAttribute: React.PropTypes.func,
didFetchModel: React.PropTypes.bool,
location: React.PropTypes.object,
@ -259,6 +267,7 @@ ModelPage.propTypes = {
modelPage: React.PropTypes.object,
params: React.PropTypes.object,
route: React.PropTypes.object,
submit: React.PropTypes.func,
};
export default connect(mapStateToProps, mapDispatchToProps)(ModelPage);

View File

@ -7,27 +7,40 @@
import { fromJS, Map, List } from 'immutable';
/* eslint-disable new-cap */
import {
ADD_ATTRIBUTE_TO_CONTENT_TYPE,
CANCEL_CHANGES,
DELETE_ATTRIBUTE,
MODEL_FETCH_SUCCEEDED,
POST_CONTENT_TYPE_SUCCEEDED,
} from './constants';
const initialState = fromJS({
initialModel: Map({
attributes: List(),
}),
model: Map({
attributes: List(),
}),
postContentTypeSuccess: false,
});
function modelPageReducer(state = initialState, action) {
switch (action.type) {
case ADD_ATTRIBUTE_TO_CONTENT_TYPE:
return state.updateIn(['model', 'attributes'], (list) => list.push(action.newAttribute));
case CANCEL_CHANGES:
return state.set('model', state.get('initialModel'));
case DELETE_ATTRIBUTE:
// console.log(action.position);
// console.log(state.getIn(['model', 'attributes']))
return state
.updateIn(['model', 'attributes'], (list) => list.splice(action.position, 1));
case MODEL_FETCH_SUCCEEDED:
return state
.set('model', Map(action.model.model))
.setIn(['model', 'attributes'], List(action.model.model.attributes));
.set('initialModel', Map(action.model.model))
.setIn(['model', 'attributes'], List(action.model.model.attributes))
.setIn(['initialModel', 'attributes'], List(action.model.model.attributes));
case POST_CONTENT_TYPE_SUCCEEDED:
return state.set('postContentTypeSuccess', !state.get('postContentTypeSuccess'));
default:
return state;
}

View File

@ -1,9 +1,16 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import { get } from 'lodash';
import { takeLatest } from 'redux-saga';
import { call, take, put, fork, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import { DELETE_ATTRIBUTE, MODEL_FETCH } from './constants';
import { modelFetchSucceeded } from './actions';
import { temporaryContentTypePosted } from 'containers/App/actions';
import { storeData } from '../../utils/storeData';
import { DELETE_ATTRIBUTE, MODEL_FETCH, SUBMIT } from './constants';
import { modelFetchSucceeded, postContentTypeSucceeded } from './actions';
import { makeSelectModel } from './selectors';
// Individual exports for testing
@ -37,14 +44,40 @@ export function* fetchModel(action) {
}
}
export function* submitChanges() {
try {
const modelName = get(storeData.getContentType(), 'name');
const body = yield select(makeSelectModel());
const method = modelName === body.name ? 'POST' : 'PUT';
const baseUrl = '/content-type-builder/models/';
const requestUrl = method === 'POST' ? baseUrl : `${baseUrl}${body.name}`;
const opts = { method, body };
yield call(request, requestUrl, opts);
if (method === 'POST') {
storeData.clearAppStorage();
yield put(temporaryContentTypePosted());
yield put(postContentTypeSucceeded());
}
} catch(error) {
console.log(error);
}
}
export function* defaultSaga() {
const loadModelWatcher = yield fork(takeLatest, MODEL_FETCH, fetchModel);
const deleteAttributeWatcher = yield fork(takeLatest, DELETE_ATTRIBUTE, attributeDelete);
const loadSubmitChanges = yield fork(takeLatest, SUBMIT, submitChanges);
yield take(LOCATION_CHANGE);
yield cancel(loadModelWatcher);
yield cancel(deleteAttributeWatcher);
yield cancel(loadSubmitChanges);
}
// All sagas to be loaded

View File

@ -23,6 +23,9 @@
"form.attribute.item.requiredField.description": "You won't be able to create an entry if this field is empty",
"form.attribute.item.uniqueField.description": "You won't be able to create an entry if there is an existing entry with identical content",
"form.button.cancel": "Cancel",
"form.button.save": "Save",
"form.contentType.item.connections": "Connection",
"form.contentType.item.name": "Name",
"form.contentType.item.description": "Description",