Commit 1946dbe9 authored by Florent Chehab's avatar Florent Chehab

All editors are back and started redisign of fields

parent 56e9236a
......@@ -40,6 +40,6 @@ module.exports = {
],
"react/no-unescaped-entities": "off", // that one doesn't improve code readability
"react/prop-types": "error",
"react/no-deprecated": "warn"
"react/no-deprecated": "error"
}
};
......@@ -5,19 +5,21 @@ 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 getMapStateToPropsForEditor from "../shared/editorFunctions/getMapStateToPropsForEditor";
import getMapDispatchToPropsForEditor from "../shared/editorFunctions/getMapDispatchToPropsForEditor";
import { withSnackbar } from "notistack";
const styles = theme => ({
...editorStyle(theme)
});
class CountryDriEditor extends Editor {
renderEditor() {
class CountryDriForm extends Form {
render() {
return (
<div>
{this.renderTitleField()}
......@@ -30,12 +32,24 @@ class CountryDriEditor extends Editor {
}
}
class CountryDriEditor extends Editor {
renderForm() {
return <CountryDriForm
modelData={this.props.modelData}
outsideData={this.props.outsideData}
ref={this.formRef}
/>;
}
}
CountryDriEditor.propTypes = {
modelData: PropTypes.object.isRequired,
};
export default compose(
withSnackbar,
withStyles(styles, { withTheme: true }),
connect(
getMapStateToPropsForEditor("countryDri"),
......
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 ScholarshipsEditor from "../shared/ScholarshipsEditor";
import Editor from "../shared/Editor";
import ScholarshipForm from "../shared/ScholarshipForm";
import editorStyle from "../shared/editorStyle";
import getMapStateToPropsForEditor from "../shared/editorFunctions/getMapStateToPropsForEditor";
import getMapDispatchToPropsForEditor from "../shared/editorFunctions/getMapDispatchToPropsForEditor";
import { withSnackbar } from "notistack";
const styles = theme => ({
...editorStyle(theme)
});
class CountryScholarshipsEditor extends ScholarshipsEditor {
class CountryScholarshipForm extends ScholarshipForm {
renderLinkedField() {
return this.renderCountriesField();
}
}
CountryScholarshipsEditor.propTypes = {
class CountryScholarshipEditor extends Editor {
renderForm() {
return (
<CountryScholarshipForm
modelData={this.props.modelData}
outsideData={this.props.outsideData}
ref={this.formRef}
/>
);
}
}
CountryScholarshipEditor.propTypes = {
modelData: PropTypes.object.isRequired,
outsideData: PropTypes.object.isRequired,
};
export default compose(
withSnackbar,
withStyles(styles, { withTheme: true }),
connect(
getMapStateToPropsForEditor("countryScholarships"),
getMapDispatchToPropsForEditor("countryScholarships")
)
)(CountryScholarshipsEditor);
)(CountryScholarshipEditor);
......@@ -43,6 +43,7 @@ class UniversityDriEditor extends Editor {
UniversityDriEditor.propTypes = {
modelData: PropTypes.object.isRequired,
outsideData: PropTypes.object.isRequired,
};
......
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 ScholarshipsEditor from "../shared/ScholarshipsEditor";
import Editor from "../shared/Editor";
import ScholarshipForm from "../shared/ScholarshipForm";
import editorStyle from "../shared/editorStyle";
import getMapStateToPropsForEditor from "../shared/editorFunctions/getMapStateToPropsForEditor";
import getMapDispatchToPropsForEditor from "../shared/editorFunctions/getMapDispatchToPropsForEditor";
import { withSnackbar } from "notistack";
const styles = theme => ({
...editorStyle(theme)
});
class UniversityScholarshipsEditor extends ScholarshipsEditor {
class UniversityScholarshipForm extends ScholarshipForm {
renderLinkedField() {
return this.renderUniversitiesField();
}
}
UniversityScholarshipsEditor.propTypes = {
class UniversityScholarshipEditor extends Editor {
renderForm() {
return (
<UniversityScholarshipForm
modelData={this.props.modelData}
outsideData={this.props.outsideData}
ref={this.formRef}
/>
);
}
}
UniversityScholarshipEditor.propTypes = {
modelData: PropTypes.object.isRequired,
outsideData: PropTypes.object.isRequired,
};
export default compose(
withSnackbar,
withStyles(styles, { withTheme: true }),
connect(
getMapStateToPropsForEditor("universityScholarships"),
getMapDispatchToPropsForEditor("universityScholarships")
)
)(UniversityScholarshipsEditor);
)(UniversityScholarshipEditor);
......@@ -23,7 +23,7 @@ const styles = theme => ({
class UniversitySemestersDatesForm extends Form {
hasError() {
hasFormLevelErrors() {
let messages = Array();
const formData = this.getDataFromFields();
const { autumn_begin, autumn_end, spring_begin, spring_end } = formData;
......@@ -44,10 +44,7 @@ class UniversitySemestersDatesForm extends Form {
messages.push("Le début du semestre de printemps doit être antérieur à sa fin...");
}
const formLevelErrors = this.buildError(messages),
fieldLevelErrors = super.hasError();
return this.combineErrors([fieldLevelErrors, formLevelErrors]);
return this.buildError(messages);
}
render() {
......
......@@ -11,7 +11,7 @@ import ModuleWrapper from "../shared/ModuleWrapper";
import ModuleGroupWrapper from "../shared/ModuleGroupWrapper";
import Scholarship from "../shared/Scholarship";
import CountryScholarshipsEditor from "../editors/CountryScholarshipsEditor";
import CountryScholarshipEditor from "../editors/CountryScholarshipEditor";
import getActions from "../../../api/getActions";
......@@ -78,7 +78,7 @@ class CountryScholarships extends Module {
<ModuleGroupWrapper
groupTitle={`Bourses liées au pays (${this.props.country.name})`}
endPoint={"countryScholarship"}
editor={CountryScholarshipsEditor}
editor={CountryScholarshipEditor}
invalidateGroup={this.props.invalidateData}
propsForEditor={{
modelData: { countries: [this.props.countryId], importance_level: "-", currency: "EUR", obj_moderation_level: 0 },
......@@ -94,7 +94,7 @@ class CountryScholarships extends Module {
buildTitle={(modelData) => modelData.title}
rawModelData={rawModelData}
parseRawModelData={parseRawModelData}
editor={CountryScholarshipsEditor}
editor={CountryScholarshipEditor}
renderCore={renderCore}
coreClasses={classes}
outsideData={outsideData}
......
......@@ -11,7 +11,7 @@ import ModuleWrapper from "../shared/ModuleWrapper";
import ModuleGroupWrapper from "../shared/ModuleGroupWrapper";
import Scholarship from "../shared/Scholarship";
import UniversityScholarshipsEditor from "../editors/UniversityScholarshipsEditor";
import UniversityScholarshipEditor from "../editors/UniversityScholarshipEditor";
import getActions from "../../../api/getActions";
// eslint-disable-next-line no-unused-vars
......@@ -76,7 +76,7 @@ class UniversityScholarships extends Module {
<ModuleGroupWrapper
groupTitle={"Bourses liées à l'université"}
endPoint={"universityScholarship"}
editor={UniversityScholarshipsEditor}
editor={UniversityScholarshipEditor}
invalidateGroup={this.props.invalidateData}
propsForEditor={{
modelData: { universities: [this.props.univId], importance_level: "-", currency: "EUR", obj_moderation_level: 0 },
......@@ -92,7 +92,7 @@ class UniversityScholarships extends Module {
buildTitle={(modelData) => modelData.title}
rawModelData={rawModelData}
parseRawModelData={parseRawModelData}
editor={UniversityScholarshipsEditor}
editor={UniversityScholarshipEditor}
renderCore={renderCore}
coreClasses={classes}
outsideData={outsideData}
......
import React, { Component } from "react";
import { Component } from "react";
import PropTypes from "prop-types";
import areSameObjects from "../../../utils/areSameObjects";
......@@ -108,21 +108,36 @@ class Form extends Component {
*/
fieldsHaveError() {
const messages = this.getFields()
.map(({ field }) => field.getError().messages)
.map(({ field }) => field.hasError().messages)
.filter(messages => messages.length > 0);
return this.buildError(messages);
}
/**
* Function to know if the form has errors.
* Function to know if the form has errors either at the field level
* Or when running checks that combine fields
*
* It can be override in subclasses to handle errors at the form level
* (multiple fields checks)
* YOU SHOULDN'T OVERRIDE THIS
*
* @returns
* @memberof Form
*/
hasError() {
return this.combineErrors([
this.fieldsHaveError(),
this.hasFormLevelErrors()
]);
}
/**
* Function to check for errors in the form
*
* Can be override in sub classes
*
* @returns object built with this.buildError
* @memberof Form
*/
hasFormLevelErrors() {
return this.fieldsHaveError();
}
......
import React from "react";
import PropTypes from "prop-types";
import Editor from "./Editor";
import Form from "./Form";
const frequencyOptions = [
{ value: "w", label: "Il s'agit du montant hebdomadaire" },
......@@ -11,18 +11,20 @@ const frequencyOptions = [
{ value: "o", label: "Il s'agit d'un montant donné une seule fois" }
];
class ScholarshipEditor extends Editor {
formHasError() {
class ScholarshipForm extends Form {
hasFormLevelErrors() {
let messages = Array();
const formData = this.getDataFromFields();
const { amount_min, amount_max } = formData;
if (amount_min !== null && amount_max !== null && amount_max < amount_min) {
messages.push("La logique voudrait que la borne supérieure de la bourse soit... supérieure à la borne inférieure.");
}
return this.buildError(messages);
}
renderEditor() {
render() {
return (
<div>
{this.renderObjModerationLevelField()}
......@@ -49,6 +51,7 @@ class ScholarshipEditor extends Editor {
label: "Fréquence de la bourse",
options: frequencyOptions,
fieldMapping: "frequency",
required: true, //TODO remove
})}
{this.renderMarkdownField({
label: "Autres avantages",
......@@ -63,8 +66,8 @@ class ScholarshipEditor extends Editor {
}
}
ScholarshipEditor.propTypes = {
ScholarshipForm.propTypes = {
modelData: PropTypes.object.isRequired,
};
export default ScholarshipEditor;
export default ScholarshipForm;
......@@ -9,7 +9,6 @@ import format from "date-fns/format";
import KeyboardArrowLeftIcon from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRightIcon from "@material-ui/icons/KeyboardArrowRight";
import FieldWrapper from "./FieldWrapper";
import dateToDateStr from "../../../../utils/dateToDateStr";
import Field from "./Field";
......@@ -33,7 +32,8 @@ class DateField extends Field {
return dateToDateStr(this.state.value);
}
hasError(date) {
hasError() {
const date = this.state.value;
let messages = Array();
if (this.props.required && !date) {
messages.push("Date requise !");
......@@ -47,27 +47,20 @@ class DateField extends Field {
});
}
render() {
renderField() {
return (
<FieldWrapper
required={this.props.required}
hasError={this.state.error.status}
errors={this.state.error.messages}
label={this.props.label}
>
<MuiPickersUtilsProvider utils={LocalizedUtils} locale={frLocale}>
<DatePicker
clearable
format="d MMM yyyy"
value={this.state.value}
onChange={this.handleDateChange}
clearLabel="vider"
cancelLabel="annuler"
leftArrowIcon={<KeyboardArrowLeftIcon />}
rightArrowIcon={<KeyboardArrowRightIcon />}
/>
</MuiPickersUtilsProvider>
</FieldWrapper >
<MuiPickersUtilsProvider utils={LocalizedUtils} locale={frLocale}>
<DatePicker
clearable
format="d MMM yyyy"
value={this.state.value}
onChange={this.handleDateChange}
clearLabel="vider"
cancelLabel="annuler"
leftArrowIcon={<KeyboardArrowLeftIcon />}
rightArrowIcon={<KeyboardArrowRightIcon />}
/>
</MuiPickersUtilsProvider>
);
}
}
......
import React from "react";
import { PureComponent } from "react";
import PropTypes from "prop-types";
import Form from "../Form";
import FieldWrapper from "./FieldWrapper";
class Field extends PureComponent {
constructor(props) {
// Attribute that will be used to replace null value
defaultNullValue = undefined;
/**
*Creates an instance of Field.
* @param {object} props
* @param {any} customDefaultValue use this parameter when a defaultNullValue needs to be set dynamically, otherwise, use class attribute defaultNullValue
* @param {*} [customStateAttrs={}] add custom state attributes on creation
* @memberof Field
*/
constructor(props, customStateAttrs = {}) {
super(props);
// make sure to subscribe ! IMPORTANT
props.form.fieldSubscribe(props.fieldMapping, this);
let { value } = props;
if (typeof this.defaultNullValue !== "undefined" && value === null) {
value = this.defaultNullValue;
}
this.state = {
value: value,
error: this.hasError(value)
...customStateAttrs,
};
}
setState(newState) {
newState.error = this.hasError(newState.value);
let state = Object.assign({}, newState);
if (typeof this.defaultNullValue !== "undefined") {
Object.assign(state, { value: newState.value === this.defaultNullValue ? null : newState.value });
}
super.setState(newState);
}
......@@ -39,8 +52,26 @@ class Field extends PureComponent {
return this.state.value;
}
getError() {
return this.state.error;
/**
* TODO
*
* @memberof Field
*/
renderField() {
throw new Error("This method should be override in subclass of Field");
}
render() {
return (
<FieldWrapper
required={this.props.required}
hasError={this.hasError().status}
errors={this.hasError().messages}
label={this.props.label}
>
{this.renderField()}
</FieldWrapper>
);
}
}
......
......@@ -3,8 +3,6 @@ import React from "react";
import PropTypes from "prop-types";
import Grid from "@material-ui/core/Grid";
import FieldWrapper from "./FieldWrapper";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
......@@ -16,7 +14,8 @@ import Field from "./Field";
class MarkdownField extends Field {
defaultNullValue = "";
hasError(value) {
hasError() {
const { value } = this.state;
let messages = Array();
if (this.props.required && value == "") {
messages.push("Ce champ est requis mais il est vide.");
......@@ -34,57 +33,54 @@ class MarkdownField extends Field {
if (maxLength && value.length > maxLength + 1) {
value = value.substring(0, maxLength + 1);
}
this.setState({ value });
this.setState({ value: value });
}
render() {
renderField() {
const valFromState = this.state.value,
value = valFromState === null ? this.defaultNullValue : valFromState;
return (
<FieldWrapper
required={this.props.required}
hasError={this.state.error.status}
errors={this.state.error.messages}
label={this.props.label}
<Grid
container
spacing={16}
>
<Grid
container
spacing={16}
>
<Grid item xs={12} md={12} lg={6}>
<Typography variant='caption'>
<em>Saisie du texte
<Grid item xs={12} md={12} lg={6}>
<Typography variant='caption'>
<em>Saisie du texte
(ce champ supporte en grande partie la syntaxe <LinkText href="https://www.markdownguide.org/basic-syntax">Markdown</LinkText>)
</em>
</Typography>
{
this.props.maxLength ?
<Typography variant='caption'>Nombre de caractères : {this.state.value.length}/{this.props.maxLength}</Typography>
:
<div></div>
}
<TextField
placeholder={"Le champ est vide"}
fullWidth={true}
multiline={true}
value={this.state.value}
onChange={(e) => this.handleChangeValue(e.target.value)}
/>
</Grid>
<Grid item xs={12} md={12} lg={6}>
<Typography variant='caption'><em>Prévisualisation du texte</em></Typography>
{
this.state.value == "" ?
(<Typography variant='caption'>Le champ est vide</Typography>)
:
(<Markdown source={this.state.value} />)
}
</Grid>
</em>
</Typography>
{
this.props.maxLength ?
<Typography variant='caption'>Nombre de caractères : {value.length}/{this.props.maxLength}</Typography>
:
<div></div>
}
<TextField
placeholder={"Le champ est vide"}
fullWidth={true}
multiline={true}
value={value}
onChange={(e) => this.handleChangeValue(e.target.value)}
/>
</Grid>
<Grid item xs={12} md={12} lg={6}>
<Typography variant='caption'><em>Prévisualisation du texte</em></Typography>
{
value == "" ?
(<Typography variant='caption'>Le champ est vide</Typography>)
:
(<Markdown source={value} />)
}
</Grid>
</FieldWrapper>
</Grid>
);
}
}
......
......@@ -2,7 +2,6 @@
import React from "react";
import PropTypes from "prop-types";
import FieldWrapper from "./FieldWrapper";
import MenuItem from "@material-ui/core/MenuItem";
import ListItemText from "@material-ui/core/ListItemText";
import Select from "@material-ui/core/Select";
......@@ -19,7 +18,8 @@ class MultiSelectField extends Field {
props.options.map((opt) => this.optionsByValue[opt.value] = opt.label);
}
hasError(value) {
hasError() {
const { value } = this.state;
let messages = Array();
if (this.props.required && value.length === 0) {
messages.push("Ce champ est requis.");
......@@ -31,31 +31,23 @@ class MultiSelectField extends Field {
this.setState({ value });
}
render() {
renderField() {
return (
<FieldWrapper
required={this.props.required}
hasError={this.state.error.status}
errors={this.state.error.messages}
label={this.props.label}
<Select
value={this.state.value}
onChange={(e) => this.handleChangeValue(e.target.value)}
multiple
renderValue={selected => selected.map((el) => this.optionsByValue[el]).join(", ")}
>
<Select
value={this.state.value}
onChange={(e) => this.handleChangeValue(e.target.value)}
multiple
renderValue={selected => selected.map((el) => this.optionsByValue[el]).join(", ")}
>
{
this.props.options.map((el) => (
<MenuItem disabled={false || el.disabled} key={el.value} value={el.value}>
<Checkbox checked={this.state.value.indexOf(el.value) > -1} />
<ListItemText primary={el.label} />
</MenuItem>
))
}
</Select>
</FieldWrapper>
{
this.props.options.map((el) => (
<MenuItem disabled={false || el.disabled} key={el.value} value={el.value}>
<Checkbox checked={this.state.value.indexOf(el.value) > -1} />
<ListItemText primary={el.label} />
</MenuItem>
))
}
</Select>
);
}
}
......
import React from "react";
import PropTypes from "prop-types";
import FieldWrapper from "./FieldWrapper";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
......@@ -12,16 +10,8 @@ import Field from "./Field";
class NumberField extends Field {
defaultNullValue = "";
getValue() {
hasError() {
const { value } = this.state;
if (value === "") {