Commit 4235b060 authored by Florent Chehab's avatar Florent Chehab

New Editor setup operationnal on universityGeneralInfo

- Form component created
- Functions migrated and rewritten for cleaner ES6 use

- Notifications not working
parent ff8b40ab
Pipeline #35618 passed with stages
in 5 minutes and 9 seconds
/**
* This file contains utilities linked to the use of the API.
*/
// Stores the name of the reducers/actions that result in read data
export const successActionsWithReads = ["readSucceeded", "createSucceeded", "updateSucceeded"];
/**
* Smartly retrieve the latest read (create and update included) data from the server
*
* @export
* @param {object} stateExtract
*/
export function getLatestRead(stateExtract) {
return successActionsWithReads
.filter(action => action in stateExtract) // general handling of all types of API reducers
.map(action => stateExtract[action])
.reduce(
(prev, curr) => prev.readAt < curr.readAt ? curr : prev,
{ readAt: 0 });
}
import React, { Component } from "react";
import Loading from "./other/Loading";
import PropTypes from "prop-types";
// Stores the name of the reducers/actions that result in read data
const successActionsWithReads = ["readSucceeded", "createSucceeded", "updateSucceeded"];
import { successActionsWithReads, getLatestRead } from "../api/utils";
class CustomComponentForAPI extends Component {
customErrorHandlers = {}
......@@ -166,16 +163,8 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
getReadDataAndTime(propName) {
const prop = this.props[propName];
// Smartly retrieve the latest data
// Stores the name of the reducers/actions that result in read data in this case
const out = successActionsWithReads
.filter(action => action in prop) // general handling of all types of API reducers
.map(action => prop[action])
.reduce(
(prev, curr) => prev.readAt < curr.readAt ? curr : prev,
{ readAt: 0 });
const prop = this.props[propName],
out = getLatestRead(prop);
if (!("data" in out)) {
throw Error(`No read data from the api could be retrieved for: ${propName}`);
......
import React from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose";
import { connect } from "react-redux";
import Editor from "../shared/Editor";
import Form from "../shared/Form";
import editorStyle from "../shared/editorStyle";
import TextField from "../shared/fields/TextField";
......@@ -16,38 +16,30 @@ const styles = theme => ({
...editorStyle(theme)
});
class UniversityGeneral extends Editor {
renderEditor() {
const { modelData } = this.props;
class UniversityGeneralForm extends Form {
render() {
return (
<div>
<TextField label={"Nom de l'université"}
value={modelData.name}
{...this.getReferenceAndValue("name")}
required={true}
maxLength={200}
formManager={this}
fieldMapping={"name"}
/>
<TextField label={"Acronyme de l'université"}
value={modelData.acronym}
{...this.getReferenceAndValue("acronym")}
maxLength={20}
formManager={this}
fieldMapping={"acronym"}
/>
<TextField label={"Site internet de l'université"}
value={modelData.website}
{...this.getReferenceAndValue("website")}
maxLength={300}
isUrl={true}
formManager={this}
fieldMapping={"website"}
/>
<TextField label={"Logo de l'université"}
value={modelData.logo}
{...this.getReferenceAndValue("logo")}
maxLength={300}
isUrl={true}
urlExtensions={["jpg", "png", "svg"]}
formManager={this}
fieldMapping={"logo"}
/>
</div>
);
......@@ -55,9 +47,14 @@ class UniversityGeneral extends Editor {
}
UniversityGeneral.propTypes = {
modelData: PropTypes.object.isRequired,
};
class UniversityGeneralEditor extends Editor {
renderForm() {
return <UniversityGeneralForm
modelData={this.props.modelData}
ref={this.formRef}
/>;
}
}
export default compose(
......@@ -66,4 +63,4 @@ export default compose(
getMapStateToPropsForEditor("universities"),
getMapDispatchToPropsForEditor("universities")
)
)(UniversityGeneral);
\ No newline at end of file
)(UniversityGeneralEditor);
import React, { Component } from "react";
import PropTypes from "prop-types";
import areSameObjects from "../../../utils/areSameObjects";
/**
* React component that should contain `Field` instances.
* Custom function have been implemented to ease form handling.
*
* @class Form
* @extends {React.Component}
*/
class Form extends Component {
// Store it as an object to make sure we don't have issues
// with resetting it for some reason
fields = Object();
/**
* This method MUST be used on all field inside a `Form` instance.
*
* Function that returns the value corresponding to `fieldMapping` and
* a reference for the field. This reference is stored in the Form class so that
* we can easily access the fields value from the form.
*
* @param {string} fieldMapping
* @memberof Form
*/
getReferenceAndValue(fieldMapping) {
const ref = React.createRef();
this.fields[fieldMapping] = ref; // store the reference for later use
return { value: this.props.modelData[fieldMapping], ref };
}
/**
* Function that returns the fields contained in the form
* as an array of {fieldMapping: string, field: Field}
*
* Works only if the `getReferenceAndValue` was used on the Field props.
*
* @returns {Array}
* @memberof Form
*/
getFields() {
return Object.keys(this.fields)
.map(fieldMapping =>
({ fieldMapping, field: this.fields[fieldMapping].current })
);
}
/**
* Returns an object containing with {fieldMapping: valueInField}
*
* @returns {Object}
* @memberof Form
*/
getDataFromFields() {
return this.getFields()
.reduce(
(acc, { field, fieldMapping }) => {
acc[fieldMapping] = field.getValue();
return acc;
},
Object());
}
/**
* Function to build a generic way of handling field errors
*
* @param {Array} messages
* @returns
* @memberof Form
*/
buildError(messages) {
return { status: messages.length > 0, messages };
}
/**
* Function to build all the errors from the fields of the form.
*
* @returns
* @memberof Form
*/
fieldsHaveError() {
const messages = this.getFields()
.map(({ field }) => field.getError().messages)
.filter(messages => messages.length > 0);
return this.buildError(messages);
}
/**
* Function to know if the form has errors.
*
* It can be override in subclasses to handle errors at the form level
* (multiple fields checks)
*
* @returns
* @memberof Form
*/
hasError() {
return this.fieldsHaveError();
}
/**
* Function to look if there has been changes compared to the data
* this is already saved.
*
* @returns {Boolean}
* @memberof Form
*/
hasChanges() {
const formData = this.getDataFromFields(),
modelData = this.props.modelData;
return Object.keys(formData).some(fieldMapping => {
const cmp1 = formData[fieldMapping],
cmp2 = modelData[fieldMapping];
// we need to compare objects (ie JSON objects) differently
if (typeof cmp1 === "object") {
return !areSameObjects(cmp1, cmp2);
} else {
return cmp1 !== cmp2;
}
});
}
}
Form.propTypes = {
modelData: PropTypes.object.isRequired,
};
export default Form;
......@@ -86,7 +86,7 @@ class Notification extends React.Component {
Notification.defaultProps = {
open: false,
duration: null,
duration: 5000,
message: "",
preventClickAway: true,
success: false,
......
import getActions from "../../../../api/getActions";
/**
* Function to create the mapDispatchToProps function for editor in a "generic way"
*
* @export
* @param {string} name
* @returns
*/
export default function getMapDispatchToPropsForEditor(name) {
return (dispatch) => {
return {
......@@ -7,4 +14,4 @@ export default function getMapDispatchToPropsForEditor(name) {
// clearSaveError: () => dispatch(savingHasErrorAction(false))
};
};
}
\ No newline at end of file
}
import { getLatestRead } from "../../../../api/utils";
// TODO check this function
/**
* Function to create the mapStateToProps function for editor in a "generic way"
*
* @export
* @param {string} elKey
* @returns
*/
export default function getMapStateToPropsForEditor(elKey) {
const propName = elKey + "Specific";
const propName = `${elKey}Specific`;
return (state) => {
let lastUpdateTime = null;
const tmp = state.api[propName].readSucceeded;
if (tmp.readAt !== 0) {
lastUpdateTime = tmp.data.updated_on;
let lastUpdateTimeInModel = null;
// We make sure to get the latest data
const latestRead = getLatestRead(state.api[propName]);
if (latestRead.readAt !== 0) {
lastUpdateTimeInModel = latestRead.data.updated_on;
}
return {
savingHasError: state.api[propName].updateFailed,
lastSaveTime: state.api[propName].updateSucceeded.readAt,
lastUpdateTime,
lastUpdateTimeInModel,
};
};
}
\ No newline at end of file
}
......@@ -39,14 +39,6 @@ class Field extends PureComponent {
return this.state.error;
}
componentDidMount() {
this.props.formManager.addField(this, this.props.fieldMapping);
}
componentWillUnmount() {
this.props.formManager.removeField(this.props.fieldMapping);
}
}
Field.defaultProps = {
......@@ -57,9 +49,7 @@ Field.defaultProps = {
Field.propTypes = {
required: PropTypes.bool.isRequired,
label: PropTypes.string,
formManager: PropTypes.object.isRequired,
fieldMapping: PropTypes.string.isRequired,
value: PropTypes.isRequired
value: PropTypes.isRequired,
};
export default Field;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment